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); }