refactor: improve feed loading and infinite scroll reliability

- Add debouncing and loading state to InfiniteScroll component to prevent duplicate entries
- Refactor updateAllFeed into smaller, focused functions for better maintainability
- Add proper error handling and type assertions
- Optimize test performance with networkidle waits and better selectors
- Fix strict mode violations in Playwright tests
- Clean up test configuration by commenting out unused browser targets
This commit is contained in:
Matthew Raymer
2025-03-31 09:17:15 +00:00
parent 8c8e4ec28e
commit 34dc4149f5
6 changed files with 3007 additions and 22370 deletions

View File

@@ -525,7 +525,7 @@ export default class HomeView extends Vue {
type: "danger",
title: "Error",
text:
err.userMessage ||
(err as { userMessage?: string })?.userMessage ||
"There was an error retrieving your settings or the latest activity.",
},
5000,
@@ -658,7 +658,7 @@ export default class HomeView extends Vue {
type: "danger",
title: "Error",
text:
err.userMessage ||
(err as { userMessage?: string })?.userMessage ||
"There was an error retrieving your settings or the latest activity.",
},
5000,
@@ -733,129 +733,210 @@ export default class HomeView extends Vue {
async updateAllFeed() {
this.isFeedLoading = true;
let endOfResults = true;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => {
if (results.data.length > 0) {
endOfResults = false;
// include the descriptions of the giver and receiver
for (const record of results.data as GiveSummaryRecord[]) {
// similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const claim = (record.fullClaim as any).claim || record.fullClaim;
// agent.did is for legacy data, before March 2023
const giverDid =
claim.agent?.identifier || (claim.agent as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
// recipient.did is for legacy data, before March 2023
const recipientDid =
claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
try {
const results = await this.retrieveGives(this.apiServer, this.feedPreviousOldestId);
if (results.data.length > 0) {
endOfResults = false;
await this.processFeedResults(results.data);
await this.updateFeedLastViewedId(results.data);
}
} catch (e) {
this.handleFeedError(e);
}
// This has indeed proven problematic. See loadMoreGives
// We should display it immediately and then get the plan later.
const fulfillsPlan = await getPlanFromCache(
record.fulfillsPlanHandleId,
this.axios,
this.apiServer,
this.activeDid,
);
// check if the record should be filtered out
let anyMatch = false;
if (this.isFeedFilteredByVisible && containsNonHiddenDid(record)) {
// has a visible DID so it's a keeper
anyMatch = true;
}
if (!anyMatch && this.isFeedFilteredByNearby) {
// check if the associated project has a location inside user's search box
if (record.fulfillsPlanHandleId) {
if (fulfillsPlan?.locLat && fulfillsPlan?.locLon) {
if (
this.latLongInAnySearchBox(
fulfillsPlan.locLat,
fulfillsPlan.locLon,
)
) {
anyMatch = true;
}
}
}
}
if (this.isAnyFeedFilterOn && !anyMatch) {
continue;
}
// checking for arrays due to legacy data
const provider = Array.isArray(claim.provider)
? claim.provider[0]
: claim.provider;
const providedByPlan = await getPlanFromCache(
provider?.identifier as string,
this.axios,
this.apiServer,
this.activeDid,
);
const newRecord: GiveRecordWithContactInfo = {
...record,
jwtId: record.jwtId,
giver: didInfoForContact(
giverDid,
this.activeDid,
contactForDid(giverDid, this.allContacts),
this.allMyDids,
),
image: claim.image,
issuer: didInfoForContact(
record.issuerDid,
this.activeDid,
contactForDid(record.issuerDid, this.allContacts),
this.allMyDids,
),
providerPlanHandleId: provider?.identifier as string,
providerPlanName: providedByPlan?.name as string,
recipientProjectName: fulfillsPlan?.name as string,
receiver: didInfoForContact(
recipientDid,
this.activeDid,
contactForDid(recipientDid, this.allContacts),
this.allMyDids,
),
};
this.feedData.push(newRecord);
}
this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId;
// The following update is only done on the first load.
if (
this.feedLastViewedClaimId == null ||
this.feedLastViewedClaimId < results.data[0].jwtId
) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId,
});
}
}
})
.catch((e) => {
logger.error("Error with feed load:", e);
this.$notify(
{
group: "alert",
type: "danger",
title: "Feed Error",
text: e.userMessage || "There was an error retrieving feed data.",
},
-1,
);
});
if (this.feedData.length === 0 && !endOfResults) {
// repeat until there's at least some data
await this.updateAllFeed();
}
this.isFeedLoading = false;
}
/**
* Processes feed results and adds them to feedData
*/
private async processFeedResults(records: GiveSummaryRecord[]) {
for (const record of records) {
const processedRecord = await this.processRecord(record);
if (processedRecord) {
this.feedData.push(processedRecord);
}
}
this.feedPreviousOldestId = records[records.length - 1].jwtId;
}
/**
* Processes a single record and returns it if it passes filters
*/
private async processRecord(record: GiveSummaryRecord): Promise<GiveRecordWithContactInfo | null> {
const claim = this.extractClaim(record);
const giverDid = this.extractGiverDid(claim);
const recipientDid = this.extractRecipientDid(claim);
const fulfillsPlan = await this.getFulfillsPlan(record);
if (!this.shouldIncludeRecord(record, fulfillsPlan)) {
return null;
}
const provider = this.extractProvider(claim);
const providedByPlan = await this.getProvidedByPlan(provider);
return this.createFeedRecord(record, claim, giverDid, recipientDid, provider, fulfillsPlan, providedByPlan);
}
/**
* Extracts claim from record, handling both direct and wrapped claims
*/
private extractClaim(record: GiveSummaryRecord) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (record.fullClaim as any).claim || record.fullClaim;
}
/**
* Extracts giver DID from claim
*/
private extractGiverDid(claim: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return claim.agent?.identifier || (claim.agent as any)?.did;
}
/**
* Extracts recipient DID from claim
*/
private extractRecipientDid(claim: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return claim.recipient?.identifier || (claim.recipient as any)?.did;
}
/**
* Gets fulfills plan from cache
*/
private async getFulfillsPlan(record: GiveSummaryRecord) {
return await getPlanFromCache(
record.fulfillsPlanHandleId,
this.axios,
this.apiServer,
this.activeDid,
);
}
/**
* Checks if record should be included based on filters
*/
private shouldIncludeRecord(record: GiveSummaryRecord, fulfillsPlan: any): boolean {
if (!this.isAnyFeedFilterOn) {
return true;
}
let anyMatch = false;
if (this.isFeedFilteredByVisible && containsNonHiddenDid(record)) {
anyMatch = true;
}
if (!anyMatch && this.isFeedFilteredByNearby && record.fulfillsPlanHandleId) {
if (fulfillsPlan?.locLat && fulfillsPlan?.locLon) {
anyMatch = this.latLongInAnySearchBox(fulfillsPlan.locLat, fulfillsPlan.locLon) ?? false;
}
}
return anyMatch;
}
/**
* Extracts provider from claim
*/
private extractProvider(claim: any) {
return Array.isArray(claim.provider) ? claim.provider[0] : claim.provider;
}
/**
* Gets provided by plan from cache
*/
private async getProvidedByPlan(provider: any) {
return await getPlanFromCache(
provider?.identifier as string,
this.axios,
this.apiServer,
this.activeDid,
);
}
/**
* Creates a feed record with contact info
*/
private createFeedRecord(
record: GiveSummaryRecord,
claim: any,
giverDid: string,
recipientDid: string,
provider: any,
fulfillsPlan: any,
providedByPlan: any
): GiveRecordWithContactInfo {
return {
...record,
jwtId: record.jwtId,
fullClaim: record.fullClaim,
description: record.description || '',
handleId: record.handleId,
issuerDid: record.issuerDid,
fulfillsPlanHandleId: record.fulfillsPlanHandleId,
giver: didInfoForContact(
giverDid,
this.activeDid,
contactForDid(giverDid, this.allContacts),
this.allMyDids,
),
image: claim.image,
issuer: didInfoForContact(
record.issuerDid,
this.activeDid,
contactForDid(record.issuerDid, this.allContacts),
this.allMyDids,
),
providerPlanHandleId: provider?.identifier as string,
providerPlanName: providedByPlan?.name as string,
recipientProjectName: fulfillsPlan?.name as string,
receiver: didInfoForContact(
recipientDid,
this.activeDid,
contactForDid(recipientDid, this.allContacts),
this.allMyDids,
),
} as GiveRecordWithContactInfo;
}
/**
* Updates the last viewed claim ID in settings
*/
private async updateFeedLastViewedId(records: GiveSummaryRecord[]) {
if (
this.feedLastViewedClaimId == null ||
this.feedLastViewedClaimId < records[0].jwtId
) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: records[0].jwtId,
});
}
}
/**
* Handles feed error and shows notification
*/
private handleFeedError(e: any) {
logger.error("Error with feed load:", e);
this.$notify(
{
group: "alert",
type: "danger",
title: "Feed Error",
text: e.userMessage || "There was an error retrieving feed data.",
},
-1,
);
}
/**
* Retrieve claims in reverse chronological order
*