From d14431161a6f8ab9e2f30c479b3151e4007295f4 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 5 Mar 2025 06:08:08 +0000 Subject: [PATCH] refactor: Improve settings and feed handling in HomeView - Split feed initialization into separate methods - Add registration status verification - Improve error handling and notifications - Add JSDoc comments for better code documentation - Make apiServer optional in settings type The changes improve code organization by: 1. Breaking down monolithic initialization into focused methods 2. Adding proper type safety for optional settings 3. Improving error handling and user feedback 4. Adding clear documentation for methods 5. Separating concerns for feed, contacts and registration --- src/db/tables/settings.ts | 2 +- src/views/HomeView.vue | 302 +++++++++++++++++++++++++------------- 2 files changed, 201 insertions(+), 103 deletions(-) diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index 69010cb..726a41e 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -20,7 +20,7 @@ export type Settings = { // active Decentralized ID activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry - apiServer: string; // API server URL + apiServer?: string; // API server URL filterFeedByNearby?: boolean; // filter by nearby filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index aa43e33..465bb2d 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -423,7 +423,6 @@ import { getNewOffersToUser, getNewOffersToUserProjects, getPlanFromCache, - GiveSummaryRecord, } from "../libs/endorserServer"; import { generateSaveAndActivateIdentity, @@ -432,7 +431,7 @@ import { OnboardPage, registerSaveAndActivatePasskey, } from "../libs/util"; - +import { GiveSummaryRecord} from "../interfaces"; interface GiveRecordWithContactInfo extends GiveSummaryRecord { jwtId: string; giver: { @@ -450,6 +449,15 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord { }; } +/** + * HomeView - Main view component for the application's home page + * + * Workflow: + * 1. On mount, initializes user identity, settings, and data + * 2. Handles user registration status + * 3. Manages feed of activities and offers + * 4. Provides interface for creating and viewing claims + */ @Component({ components: { EntityIcon, @@ -503,127 +511,198 @@ export default class HomeView extends Vue { isImageViewerOpen = false; imageCache: Map = new Map(); + /** + * Initializes the component on mount + * Sequence: + * 1. Initialize identity (create if needed) + * 2. Load user settings + * 3. Load contacts + * 4. Check registration status + * 5. Load feed data + * 6. Load new offers + * 7. Check onboarding status + */ async mounted() { try { - try { - this.allMyDids = await retrieveAccountDids(); - if (this.allMyDids.length === 0) { - this.isCreatingIdentifier = true; - const newDid = await generateSaveAndActivateIdentity(); - this.isCreatingIdentifier = false; - this.allMyDids = [newDid]; - } - } catch (error) { - // continue because we want the feed to work, even anonymously - logConsoleAndDb( - "Error retrieving all account DIDs on home page:" + error, - true, - ); - // some other piece will display an error about personal info - } + await this.initializeIdentity(); + await this.loadSettings(); + await this.loadContacts(); + await this.checkRegistrationStatus(); + await this.loadFeedData(); + await this.loadNewOffers(); + await this.checkOnboarding(); + } catch (err: any) { + this.handleError(err); + } + } - const settings = await retrieveSettingsForActiveAccount(); - this.apiServer = settings.apiServer || ""; - this.activeDid = settings.activeDid || ""; - this.allContacts = await db.contacts.toArray(); - this.feedLastViewedClaimId = settings.lastViewedClaimId; - this.givenName = settings.firstName || ""; - this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; - this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; - this.isRegistered = !!settings.isRegistered; - this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; - this.lastAckedOfferToUserProjectsJwtId = - settings.lastAckedOfferToUserProjectsJwtId; - this.searchBoxes = settings.searchBoxes || []; - this.showShortcutBvc = !!settings.showShortcutBvc; - - this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); - - if (!settings.finishedOnboarding) { - (this.$refs.onboardingDialog as OnboardingDialog).open( - OnboardPage.Home, - ); + /** + * Initializes user identity + * - Retrieves existing DIDs + * - Creates new DID if none exists + * @throws Logs error if DID retrieval fails + */ + private async initializeIdentity() { + try { + this.allMyDids = await retrieveAccountDids(); + if (this.allMyDids.length === 0) { + this.isCreatingIdentifier = true; + const newDid = await generateSaveAndActivateIdentity(); + this.isCreatingIdentifier = false; + this.allMyDids = [newDid]; } + } catch (error) { + logConsoleAndDb("Error retrieving all account DIDs on home page:" + error, true); + } + } - // someone may have have registered after sharing contact info, so recheck - if (!this.isRegistered && this.activeDid) { - try { - const resp = await fetchEndorserRateLimits( - this.apiServer, - this.axios, - this.activeDid, - ); - if (resp.status === 200) { - await updateAccountSettings(this.activeDid, { - isRegistered: true, - }); - this.isRegistered = true; - } - } catch (e) { - // ignore the error... just keep us unregistered - } - } + /** + * Loads user settings from storage + * Sets component state for: + * - API server + * - Active DID + * - Feed filters and view settings + * - Registration status + * - Notification acknowledgments + */ + private async loadSettings() { + const settings = await retrieveSettingsForActiveAccount(); + this.apiServer = settings.apiServer || ""; + this.activeDid = settings.activeDid || ""; + this.feedLastViewedClaimId = settings.lastViewedClaimId; + this.givenName = settings.firstName || ""; + this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; + this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; + this.isRegistered = !!settings.isRegistered; + this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; + this.lastAckedOfferToUserProjectsJwtId = settings.lastAckedOfferToUserProjectsJwtId; + this.searchBoxes = settings.searchBoxes || []; + this.showShortcutBvc = !!settings.showShortcutBvc; + this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); + } - // this returns a Promise but we don't need to wait for it - this.updateAllFeed(); + /** + * Loads user contacts from database + * Used for displaying contact info in feed and actions + */ + private async loadContacts() { + this.allContacts = await db.contacts.toArray(); + } - if (this.activeDid) { - const offersToUserData = await getNewOffersToUser( - this.axios, - this.apiServer, - this.activeDid, - this.lastAckedOfferToUserJwtId, - ); - this.numNewOffersToUser = offersToUserData.data.length; - this.newOffersToUserHitLimit = offersToUserData.hitLimit; + /** + * Verifies user registration status with endorser service + * - Checks if unregistered user can access API + * - Updates registration status if successful + * - Preserves unregistered state on failure + */ + private async checkRegistrationStatus() { + if (!this.isRegistered && this.activeDid) { + try { + const resp = await fetchEndorserRateLimits(this.apiServer, this.axios, this.activeDid); + if (resp.status === 200) { + await updateAccountSettings(this.activeDid, { + apiServer: this.apiServer, + isRegistered: true, + ...await retrieveSettingsForActiveAccount() + }); + this.isRegistered = true; + } + } catch (e) { + // ignore the error... just keep us unregistered } + } + } - if (this.activeDid) { - const offersToUserProjects = await getNewOffersToUserProjects( - this.axios, - this.apiServer, - this.activeDid, - this.lastAckedOfferToUserProjectsJwtId, - ); - this.numNewOffersToUserProjects = offersToUserProjects.data.length; - this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit; - } + /** + * Initializes feed data + * Triggers updateAllFeed() to populate activity feed + */ + private async loadFeedData() { + await this.updateAllFeed(); + } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - logConsoleAndDb("Error retrieving settings or feed: " + err, true); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: - err.userMessage || - "There was an error retrieving your settings or the latest activity.", - }, - 5000, + /** + * Loads new offers for user and their projects + * Updates: + * - Number of new direct offers + * - Number of new project offers + * - Rate limit status for both + * @requires Active DID + */ + private async loadNewOffers() { + if (this.activeDid) { + const offersToUserData = await getNewOffersToUser( + this.axios, + this.apiServer, + this.activeDid, + this.lastAckedOfferToUserJwtId + ); + this.numNewOffersToUser = offersToUserData.data.length; + this.newOffersToUserHitLimit = offersToUserData.hitLimit; + + const offersToUserProjects = await getNewOffersToUserProjects( + this.axios, + this.apiServer, + this.activeDid, + this.lastAckedOfferToUserProjectsJwtId ); + this.numNewOffersToUserProjects = offersToUserProjects.data.length; + this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit; } } - async generatePasskeyIdentifier() { - this.isCreatingIdentifier = true; - const account = await registerSaveAndActivatePasskey( - AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""), + /** + * Checks if user needs onboarding + * Opens onboarding dialog if not completed + */ + private async checkOnboarding() { + const settings = await retrieveSettingsForActiveAccount(); + if (!settings.finishedOnboarding) { + (this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home); + } + } + + /** + * Handles errors during initialization + * - Logs error to console and database + * - Displays user notification + * @param err Error object with optional userMessage + */ + private handleError(err: any) { + logConsoleAndDb("Error retrieving settings or feed: " + err, true); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: err.userMessage || "There was an error retrieving your settings or the latest activity.", + }, + 5000 ); - this.activeDid = account.did; - this.allMyDids = this.allMyDids.concat(this.activeDid); - this.isCreatingIdentifier = false; } + + /** + * Checks if feed results are being filtered + * @returns true if visible or nearby filters are active + */ resultsAreFiltered() { return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby; } + /** + * Checks if browser notifications are supported + * @returns true if Notification API is available + */ notificationsSupported() { return "Notification" in window; } - // only called when a setting was changed + /** + * Reloads feed when filter settings change + * - Updates filter states + * - Clears existing feed data + * - Triggers new feed load + */ async reloadFeedOnChange() { const settings = await retrieveSettingsForActiveAccount(); this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; @@ -636,9 +715,9 @@ export default class HomeView extends Vue { } /** - * Data loader used by infinite scroller - * @param payload is the flag from the InfiniteScroll indicating if it should load - **/ + * Loads more feed items for infinite scroll + * @param payload Boolean indicating if more items should be loaded + */ async loadMoreGives(payload: boolean) { // Since feed now loads projects along the way, it takes longer // and the InfiniteScroll component triggers a load before finished. @@ -661,6 +740,12 @@ export default class HomeView extends Vue { } } + /** + * Updates feed with latest activity + * - Handles filtering of results + * - Updates last viewed claim ID + * - Manages loading state + */ async updateAllFeed() { this.isFeedLoading = true; let endOfResults = true; @@ -917,6 +1002,11 @@ export default class HomeView extends Vue { return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; } + /** + * Opens dialog for creating new gift/claim + * @param giver Optional contact info for giver + * @param description Optional gift description + */ openDialog(giver?: GiverReceiverInputInfo, description?: string) { (this.$refs.customDialog as GiftedDialog).open( giver, @@ -930,12 +1020,20 @@ export default class HomeView extends Vue { ); } + /** + * Opens prompts for gift ideas + * Links to openDialog for selected prompt + */ openGiftedPrompts() { (this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) => this.openDialog(giver as GiverReceiverInputInfo, description), ); } + /** + * Opens feed filter configuration + * @param reloadFeedOnChange Callback for when filters are updated + */ openFeedFilters() { (this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange); }