@ -223,12 +223,27 @@ Raymer * @version 1.0.0 */
< / div >
< InfiniteScroll @reached-bottom ="loadMoreGives" >
< ul id = "listLatestActivity" class = "space-y-4" >
<!-- Skeleton loading state for immediate visual feedback -- >
< div v-if ="isFeedLoading && feedData.length === 0" class="space-y-4" >
< div v-for ="i in 3" :key="`skeleton-${i}`" class="animate-pulse" >
< div class = "bg-gray-200 rounded-lg p-4" >
< div class = "flex items-center space-x-4" >
< div class = "w-12 h-12 bg-gray-300 rounded-full" > < / div >
< div class = "flex-1 space-y-2" >
< div class = "h-4 bg-gray-300 rounded w-3/4" > < / div >
< div class = "h-3 bg-gray-300 rounded w-1/2" > < / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< ActivityListItem
v - for = "record in feedData"
: key = "record.jwtId"
: record = "record"
: last - viewed - claim - id = "feedLastViewedClaimId"
: is - registered = "isRegistered"
: is - registered = "isUser Registered"
: active - did = "activeDid"
: confirmer - id - list = "record.confirmerIdList"
: on - image - cache = "cacheImageData"
@ -243,6 +258,11 @@ Raymer * @version 1.0.0 */
< font -awesome icon = "spinner" class = "fa-spin-pulse" / > Loading & hellip ;
< / p >
< / div >
< div v-if ="isBackgroundProcessing" class="mt-2" >
< p class = "text-slate-400 text-center text-sm italic" >
< font -awesome icon = "spinner" class = "fa-spin" / > Loading more content & hellip ;
< / p >
< / div >
< div v-if ="!isFeedLoading && feedData.length === 0" >
< p class = "text-slate-500 text-center italic mt-4 mb-4" >
No claims match your filters .
@ -302,7 +322,7 @@ import {
GiverReceiverInputInfo ,
OnboardPage ,
} from "../libs/util" ;
import { GiveSummaryRecord } from "../interfaces/records" ;
import { GiveSummaryRecord , PlanSummaryRecord } from "../interfaces/records" ;
import * as serverUtil from "../libs/endorserServer" ;
import { logger } from "../utils/logger" ;
import { GiveRecordWithContactInfo } from "../interfaces/give" ;
@ -414,16 +434,18 @@ export default class HomeView extends Vue {
allMyDids : Array < string > = [ ] ;
apiServer = "" ;
blockedContactDids : Array < string > = [ ] ;
/ / F e e d d a t a a n d s t a t e
feedData : GiveRecordWithContactInfo [ ] = [ ] ;
feedPreviousOldestId ? : string ;
isFeedLoading = false ;
isBackgroundProcessing = false ;
feedPreviousOldestId : string | undefined = undefined ;
feedLastViewedClaimId ? : string ;
givenName = "" ;
isRegistered = false ;
isAnyFeedFilterOn = false ;
/ / i s C r e a t i n g I d e n t i f i e r r e m o v e d - i d e n t i t y c r e a t i o n n o w h a n d l e d b y r o u t e r g u a r d
isFeedFilteredByVisible = false ;
isFeedFilteredByNearby = false ;
isFeedLoading = true ;
isRegistered = false ;
lastAckedOfferToUserJwtId ? : string ; / / t h e l a s t J W T I D f o r o f f e r - t o - u s e r t h a t t h e y ' v e a c k n o w l e d g e d s e e i n g
lastAckedOfferToUserProjectsJwtId ? : string ; / / t h e l a s t J W T I D f o r o f f e r s - t o - u s e r ' s - p r o j e c t s t h a t t h e y ' v e a c k n o w l e d g e d s e e i n g
newOffersToUserHitLimit : boolean = false ;
@ -823,9 +845,8 @@ export default class HomeView extends Vue {
}
/ * *
* Reloads feed when filter settings change using ultra - concise mixin utilities
* - Updates filter states
* - Clears existing feed data
* Reloads feed when filters change
* - Resets feed data and pagination
* - Triggers new feed load
*
* @ public
@ -840,8 +861,11 @@ export default class HomeView extends Vue {
this . isFeedFilteredByNearby = ! ! settings . filterFeedByNearby ;
this . isAnyFeedFilterOn = checkIsAnyFeedFilterOn ( settings ) ;
/ / R e s e t f e e d s t a t e t o p r e v e n t I n f i n i t e S c r o l l c o n f l i c t s
this . feedData = [ ] ;
this . feedPreviousOldestId = undefined ;
this . isBackgroundProcessing = false ;
await this . updateAllFeed ( ) ;
}
@ -853,14 +877,59 @@ export default class HomeView extends Vue {
* @ param payload Boolean indicating if more items should be loaded
* /
async loadMoreGives ( payload : boolean ) {
/ / S i n c e f e e d n o w l o a d s p r o j e c t s a l o n g t h e w a y , i t t a k e s l o n g e r
/ / a n d t h e I n f i n i t e S c r o l l c o m p o n e n t t r i g g e r s a l o a d b e f o r e f i n i s h e d .
/ / O n e a l t e r n a t i v e i s t o t o t a l l y s e p a r a t e t h e p r o j e c t l i n k l o a d i n g .
if ( payload && ! this . isFeedLoading ) {
/ / P r e v e n t l o a d i n g i f a l r e a d y p r o c e s s i n g o r i f b a c k g r o u n d p r o c e s s i n g i s a c t i v e
if ( payload && ! this . isFeedLoading && ! this . isBackgroundProcessing ) {
/ / U s e d i r e c t u p d a t e i n s t e a d o f d e b o u n c e d t o a v o i d c o n f l i c t s w i t h I n f i n i t e S c r o l l ' s d e b o u n c i n g
await this . updateAllFeed ( ) ;
}
}
/ * *
* Debounced version of updateAllFeed to prevent rapid successive calls
*
* @ internal
* @ callGraph
* Called by : loadMoreGives ( )
* Calls : updateAllFeed ( )
*
* @ chain
* loadMoreGives ( ) - > debouncedUpdateFeed ( ) - > updateAllFeed ( )
*
* @ requires
* - this . isFeedLoading
* /
private debouncedUpdateFeed = this . debounce ( async ( ) => {
if ( ! this . isFeedLoading ) {
await this . updateAllFeed ( ) ;
}
} , 300 ) ;
/ * *
* Creates a debounced function to prevent rapid successive calls
*
* @ internal
* @ callGraph
* Called by : debouncedUpdateFeed ( )
* Calls : None
*
* @ chain
* debouncedUpdateFeed ( ) - > debounce ( )
*
* @ param func Function to debounce
* @ param delay Delay in milliseconds
* @ returns Debounced function
* /
private debounce < T extends ( ...args : any [ ] ) = > any > (
func : T ,
delay : number
) : ( ... args : Parameters < T > ) => void {
let timeoutId : NodeJS . Timeout ;
return ( ... args : Parameters < T > ) => {
clearTimeout ( timeoutId ) ;
timeoutId = setTimeout ( ( ) => func ( ... args ) , delay ) ;
} ;
}
/ * *
* Checks if coordinates fall within any search box
*
@ -921,18 +990,52 @@ export default class HomeView extends Vue {
* - this . feedLastViewedClaimId ( via updateFeedLastViewedId )
* /
async updateAllFeed ( ) {
const startTime = performance . now ( ) ;
this . isFeedLoading = true ;
let endOfResults = true ;
try {
const apiStartTime = performance . now ( ) ;
const results = await this . retrieveGives (
this . apiServer ,
this . feedPreviousOldestId ,
) ;
const apiTime = performance . now ( ) - apiStartTime ;
if ( results . data . length > 0 ) {
endOfResults = false ;
/ / g a t h e r a n y c o n t a c t s t h a t u s e r h a s b l o c k e d f r o m v i e w
await this . processFeedResults ( results . data ) ;
/ / C h e c k i f w e h a v e c a c h e d d a t a f o r t h e s e r e c o r d s
const uncachedRecords = this . filterUncachedRecords ( results . data ) ;
if ( uncachedRecords . length > 0 ) {
/ / P r o c e s s f i r s t 5 r e c o r d s i m m e d i a t e l y f o r q u i c k d i s p l a y
const priorityRecords = uncachedRecords . slice ( 0 , 5 ) ;
const remainingRecords = uncachedRecords . slice ( 5 ) ;
/ / P r o c e s s p r i o r i t y r e c o r d s f i r s t
const processStartTime = performance . now ( ) ;
await this . processPriorityRecords ( priorityRecords ) ;
const processTime = performance . now ( ) - processStartTime ;
/ / P r o c e s s r e m a i n i n g r e c o r d s i n b a c k g r o u n d
if ( remainingRecords . length > 0 ) {
this . processRemainingRecords ( remainingRecords ) ;
}
/ / L o g p e r f o r m a n c e m e t r i c s i n d e v e l o p m e n t
if ( process . env . NODE_ENV === 'development' ) {
logger . debug ( '[HomeView Performance]' , {
apiTime : ` ${ apiTime . toFixed ( 2 ) } ms ` ,
processTime : ` ${ processTime . toFixed ( 2 ) } ms ` ,
priorityRecords : priorityRecords . length ,
remainingRecords : remainingRecords . length ,
totalRecords : results . data . length ,
cacheHitRate : ` ${ ( ( results . data . length - uncachedRecords . length ) / results . data . length * 100 ) . toFixed ( 1 ) } % `
} ) ;
}
}
await this . updateFeedLastViewedId ( results . data ) ;
}
} catch ( e ) {
@ -944,6 +1047,100 @@ export default class HomeView extends Vue {
}
this . isFeedLoading = false ;
const totalTime = performance . now ( ) - startTime ;
/ / L o g t o t a l p e r f o r m a n c e i n d e v e l o p m e n t
if ( process . env . NODE_ENV === 'development' ) {
logger . debug ( '[HomeView Feed Update]' , {
totalTime : ` ${ totalTime . toFixed ( 2 ) } ms ` ,
feedDataLength : this . feedData . length
} ) ;
}
}
/ * *
* Processes priority records for immediate display
*
* @ internal
* @ callGraph
* Called by : updateAllFeed ( )
* Calls : processRecordWithCache ( )
*
* @ chain
* updateAllFeed ( ) - > processPriorityRecords ( )
*
* @ param priorityRecords Array of records to process immediately
* /
private async processPriorityRecords ( priorityRecords : GiveSummaryRecord [ ] ) {
/ / F e t c h p l a n s f o r p r i o r i t y r e c o r d s o n l y
const planHandleIds = new Set < string > ( ) ;
priorityRecords . forEach ( record => {
if ( record . fulfillsPlanHandleId ) {
planHandleIds . add ( record . fulfillsPlanHandleId ) ;
}
} ) ;
const planCache = new Map < string , PlanSummaryRecord > ( ) ;
await this . batchFetchPlans ( Array . from ( planHandleIds ) , planCache ) ;
/ / P r o c e s s a n d d i s p l a y p r i o r i t y r e c o r d s i m m e d i a t e l y
for ( const record of priorityRecords ) {
const processedRecord = await this . processRecordWithCache ( record , planCache , true ) ;
if ( processedRecord ) {
await nextTick ( ( ) => {
this . feedData . push ( processedRecord ) ;
} ) ;
}
}
}
/ * *
* Processes remaining records in background
*
* @ internal
* @ callGraph
* Called by : updateAllFeed ( )
* Calls : processFeedResults ( )
*
* @ chain
* updateAllFeed ( ) - > processRemainingRecords ( )
*
* @ param remainingRecords Array of records to process in background
* /
private async processRemainingRecords ( remainingRecords : GiveSummaryRecord [ ] ) {
/ / P r o c e s s r e m a i n i n g r e c o r d s w i t h o u t b l o c k i n g t h e U I
this . isBackgroundProcessing = true ;
/ / U s e a l o n g e r d e l a y t o e n s u r e I n f i n i t e S c r o l l d o e s n ' t t r i g g e r p r e m a t u r e l y
setTimeout ( async ( ) => {
try {
await this . processFeedResults ( remainingRecords ) ;
} finally {
this . isBackgroundProcessing = false ;
}
} , 500 ) ; / / I n c r e a s e d d e l a y t o p r e v e n t c o n f l i c t s w i t h I n f i n i t e S c r o l l
}
/ * *
* Filters out records that are already cached to avoid re - processing
*
* @ internal
* @ callGraph
* Called by : updateAllFeed ( )
* Calls : None
*
* @ chain
* updateAllFeed ( ) - > filterUncachedRecords ( )
*
* @ requires
* - this . feedData
*
* @ param records Array of records to filter
* @ returns Array of records not already in feed data
* /
private filterUncachedRecords ( records : GiveSummaryRecord [ ] ) : GiveSummaryRecord [ ] {
const existingJwtIds = new Set ( this . feedData . map ( record => record . jwtId ) ) ;
return records . filter ( record => ! existingJwtIds . has ( record . jwtId ) ) ;
}
/ * *
@ -968,23 +1165,158 @@ export default class HomeView extends Vue {
* @ param records Array of feed records to process
* /
private async processFeedResults ( records : GiveSummaryRecord [ ] ) {
/ / P r e - f e t c h a l l r e q u i r e d p l a n s i n b a t c h t o r e d u c e A P I c a l l s
const planHandleIds = new Set < string > ( ) ;
records . forEach ( record => {
if ( record . fulfillsPlanHandleId ) {
planHandleIds . add ( record . fulfillsPlanHandleId ) ;
}
} ) ;
/ / B a t c h f e t c h a l l p l a n s
const planCache = new Map < string , PlanSummaryRecord > ( ) ;
await this . batchFetchPlans ( Array . from ( planHandleIds ) , planCache ) ;
/ / P r o c e s s a n d d i s p l a y r e c o r d s i m m e d i a t e l y a s t h e y ' r e r e a d y
const processedRecords : GiveRecordWithContactInfo [ ] = [ ] ;
for ( const record of records ) {
const processedRecord = await this . processRecord ( record ) ;
const processedRecord = await this . processRecordWithCache ( record , planCache ) ;
if ( processedRecord ) {
processedRecords . push ( processedRecord ) ;
/ / D i s p l a y r e c o r d s i n b a t c h e s o f 3 f o r i m m e d i a t e v i s u a l f e e d b a c k
if ( processedRecords . length % 3 === 0 ) {
await nextTick ( ( ) => {
this . feedData . push ( ... processedRecords . slice ( - 3 ) ) ;
} ) ;
}
}
}
/ / B a t c h u p d a t e t h e f e e d d a t a t o r e d u c e r e a c t i v i t y t r i g g e r s
await nextTick ( ( ) => {
this . feedData . push ( ... processedRecords ) ;
} ) ;
/ / A d d a n y r e m a i n i n g r e c o r d s
const remainingRecords = processedRecords . slice ( Math . floor ( processedRecords . length / 3 ) * 3 ) ;
if ( remainingRecords . length > 0 ) {
await nextTick ( ( ) => {
this . feedData . push ( ... remainingRecords ) ;
} ) ;
}
this . feedPreviousOldestId = records [ records . length - 1 ] . jwtId ;
}
/ * *
* Batch fetches multiple plans to reduce API calls
*
* @ internal
* @ callGraph
* Called by : processFeedResults ( )
* Calls : getPlanFromCache ( )
*
* @ chain
* processFeedResults ( ) - > batchFetchPlans ( )
*
* @ requires
* - this . axios
* - this . apiServer
* - this . activeDid
*
* @ param planHandleIds Array of plan handle IDs to fetch
* @ param planCache Map to store fetched plans
* /
private async batchFetchPlans (
planHandleIds : string [ ] ,
planCache : Map < string , PlanSummaryRecord >
) {
/ / P r o c e s s p l a n s i n b a t c h e s o f 1 0 t o a v o i d o v e r w h e l m i n g t h e A P I
const batchSize = 10 ;
for ( let i = 0 ; i < planHandleIds . length ; i += batchSize ) {
const batch = planHandleIds . slice ( i , i + batchSize ) ;
await Promise . all (
batch . map ( async ( handleId ) => {
const plan = await getPlanFromCache (
handleId ,
this . axios ,
this . apiServer ,
this . activeDid ,
) ;
if ( plan ) {
planCache . set ( handleId , plan ) ;
}
} )
) ;
}
}
/ * *
* Processes a single record with cached plans
*
* @ internal
* @ callGraph
* Called by : processFeedResults ( )
* Calls :
* - extractClaim ( )
* - extractGiverDid ( )
* - extractRecipientDid ( )
* - shouldIncludeRecord ( )
* - extractProvider ( )
* - createFeedRecord ( )
*
* @ chain
* processFeedResults ( ) - > processRecordWithCache ( )
*
* @ requires
* - this . isAnyFeedFilterOn
* - this . isFeedFilteredByVisible
* - this . isFeedFilteredByNearby
* - this . activeDid
* - this . allContacts
*
* @ param record The record to process
* @ param planCache Map of cached plans
* @ param isPriority Whether this is a priority record for quick display
* @ returns Processed record with contact info if it passes filters , null otherwise
* /
private async processRecordWithCache (
record : GiveSummaryRecord ,
planCache : Map < string , PlanSummaryRecord > ,
isPriority : boolean = false
) : Promise < GiveRecordWithContactInfo | null > {
const claim = this . extractClaim ( record ) ;
const giverDid = this . extractGiverDid ( claim ) ;
const recipientDid = this . extractRecipientDid ( claim ) ;
/ / F o r p r i o r i t y r e c o r d s , s k i p e x p e n s i v e p l a n l o o k u p s i n i t i a l l y
let fulfillsPlan : FulfillsPlan | undefined ;
if ( ! isPriority || record . fulfillsPlanHandleId ) {
fulfillsPlan = planCache . get ( record . fulfillsPlanHandleId || '' ) ||
await this . getFulfillsPlan ( record ) ;
}
if ( ! this . shouldIncludeRecord ( record , fulfillsPlan ) ) {
return null ;
}
const provider = this . extractProvider ( claim ) ;
let providedByPlan : ProvidedByPlan | undefined ;
/ / F o r p r i o r i t y r e c o r d s , d e f e r p r o v i d e r p l a n l o o k u p
if ( ! isPriority && provider ? . identifier ) {
providedByPlan = planCache . get ( provider . identifier ) ||
await this . getProvidedByPlan ( provider ) ;
}
return this . createFeedRecord (
record ,
claim ,
giverDid ,
recipientDid ,
provider ,
fulfillsPlan ,
providedByPlan ,
) ;
}
/ * *
* Processes a single record and returns it if it passes filters
*
@ -1153,34 +1485,35 @@ export default class HomeView extends Vue {
record : GiveSummaryRecord ,
fulfillsPlan ? : FulfillsPlan ,
) : boolean {
/ / E a r l y e x i t f o r b l o c k e d c o n t a c t s
if ( this . blockedContactDids . includes ( record . issuerDid ) ) {
return false ;
}
/ / I f n o f i l t e r s a r e a c t i v e , i n c l u d e a l l r e c o r d s
if ( ! this . isAnyFeedFilterOn ) {
return true ;
}
let anyMatch = false ;
/ / C h e c k v i s i b i l i t y f i l t e r f i r s t ( f a s t e r t h a n l o c a t i o n c h e c k )
if ( this . isFeedFilteredByVisible && containsNonHiddenDid ( record ) ) {
anyMatch = true ;
return true ;
}
if (
! anyMatch &&
this . isFeedFilteredByNearby &&
record . fulfillsPlanHandleId
) {
/ / C h e c k l o c a t i o n f i l t e r o n l y i f n e e d e d a n d p l a n e x i s t s
if ( this . isFeedFilteredByNearby && record . fulfillsPlanHandleId ) {
if ( fulfillsPlan ? . locLat && fulfillsPlan ? . locLon ) {
anyMatch =
this . latLongInAnySearchBox (
fulfillsPlan . locLat ,
fulfillsPlan . locLon ,
) ? ? false ;
return this . latLongInAnySearchBox (
fulfillsPlan . locLat ,
fulfillsPlan . locLon ,
) ? ? false ;
}
/ / I f p l a n e x i s t s b u t n o l o c a t i o n d a t a , e x c l u d e i t
return false ;
}
return anyMatch ;
/ / I f w e r e a c h h e r e , n o f i l t e r s m a t c h e d
return false ;
}
/ * *
@ -1809,5 +2142,28 @@ export default class HomeView extends Vue {
get isUserRegistered ( ) {
return this . isRegistered ;
}
/ * *
* Debug method to verify debugging capabilities work with optimizations
*
* @ public
* Called by : Debug testing
* @ returns Debug information
* /
debugOptimizations ( ) {
/ / T h i s m e t h o d s h o u l d b e d e b u g g a b l e w i t h b r e a k p o i n t s
const debugInfo = {
timestamp : new Date ( ) . toISOString ( ) ,
feedDataLength : this . feedData . length ,
isFeedLoading : this . isFeedLoading ,
activeDid : this . activeDid ,
performance : performance . now ( )
} ;
console . log ( '🔍 Debug Info:' , debugInfo ) ;
debugger ; / / T h i s s h o u l d t r i g g e r b r e a k p o i n t i n d e v t o o l s
return debugInfo ;
}
}
< / script >