Browse Source

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
pull/127/head
Matthew Raymer 1 week ago
parent
commit
d14431161a
  1. 2
      src/db/tables/settings.ts
  2. 302
      src/views/HomeView.vue

2
src/db/tables/settings.ts

@ -20,7 +20,7 @@ export type Settings = {
// active Decentralized ID // active Decentralized ID
activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry 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 filterFeedByNearby?: boolean; // filter by nearby
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden

302
src/views/HomeView.vue

@ -423,7 +423,6 @@ import {
getNewOffersToUser, getNewOffersToUser,
getNewOffersToUserProjects, getNewOffersToUserProjects,
getPlanFromCache, getPlanFromCache,
GiveSummaryRecord,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { import {
generateSaveAndActivateIdentity, generateSaveAndActivateIdentity,
@ -432,7 +431,7 @@ import {
OnboardPage, OnboardPage,
registerSaveAndActivatePasskey, registerSaveAndActivatePasskey,
} from "../libs/util"; } from "../libs/util";
import { GiveSummaryRecord} from "../interfaces";
interface GiveRecordWithContactInfo extends GiveSummaryRecord { interface GiveRecordWithContactInfo extends GiveSummaryRecord {
jwtId: string; jwtId: string;
giver: { 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({ @Component({
components: { components: {
EntityIcon, EntityIcon,
@ -503,127 +511,198 @@ export default class HomeView extends Vue {
isImageViewerOpen = false; isImageViewerOpen = false;
imageCache: Map<string, Blob | null> = new Map(); imageCache: Map<string, Blob | null> = 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() { async mounted() {
try { try {
try { await this.initializeIdentity();
this.allMyDids = await retrieveAccountDids(); await this.loadSettings();
if (this.allMyDids.length === 0) { await this.loadContacts();
this.isCreatingIdentifier = true; await this.checkRegistrationStatus();
const newDid = await generateSaveAndActivateIdentity(); await this.loadFeedData();
this.isCreatingIdentifier = false; await this.loadNewOffers();
this.allMyDids = [newDid]; await this.checkOnboarding();
} } catch (err: any) {
} catch (error) { this.handleError(err);
// 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
}
const settings = await retrieveSettingsForActiveAccount(); /**
this.apiServer = settings.apiServer || ""; * Initializes user identity
this.activeDid = settings.activeDid || ""; * - Retrieves existing DIDs
this.allContacts = await db.contacts.toArray(); * - Creates new DID if none exists
this.feedLastViewedClaimId = settings.lastViewedClaimId; * @throws Logs error if DID retrieval fails
this.givenName = settings.firstName || ""; */
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; private async initializeIdentity() {
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; try {
this.isRegistered = !!settings.isRegistered; this.allMyDids = await retrieveAccountDids();
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; if (this.allMyDids.length === 0) {
this.lastAckedOfferToUserProjectsJwtId = this.isCreatingIdentifier = true;
settings.lastAckedOfferToUserProjectsJwtId; const newDid = await generateSaveAndActivateIdentity();
this.searchBoxes = settings.searchBoxes || []; this.isCreatingIdentifier = false;
this.showShortcutBvc = !!settings.showShortcutBvc; this.allMyDids = [newDid];
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Home,
);
} }
} 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) { * Loads user settings from storage
try { * Sets component state for:
const resp = await fetchEndorserRateLimits( * - API server
this.apiServer, * - Active DID
this.axios, * - Feed filters and view settings
this.activeDid, * - Registration status
); * - Notification acknowledgments
if (resp.status === 200) { */
await updateAccountSettings(this.activeDid, { private async loadSettings() {
isRegistered: true, const settings = await retrieveSettingsForActiveAccount();
}); this.apiServer = settings.apiServer || "";
this.isRegistered = true; this.activeDid = settings.activeDid || "";
} this.feedLastViewedClaimId = settings.lastViewedClaimId;
} catch (e) { this.givenName = settings.firstName || "";
// ignore the error... just keep us unregistered 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( * Verifies user registration status with endorser service
this.axios, * - Checks if unregistered user can access API
this.apiServer, * - Updates registration status if successful
this.activeDid, * - Preserves unregistered state on failure
this.lastAckedOfferToUserJwtId, */
); private async checkRegistrationStatus() {
this.numNewOffersToUser = offersToUserData.data.length; if (!this.isRegistered && this.activeDid) {
this.newOffersToUserHitLimit = offersToUserData.hitLimit; 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( * Initializes feed data
this.axios, * Triggers updateAllFeed() to populate activity feed
this.apiServer, */
this.activeDid, private async loadFeedData() {
this.lastAckedOfferToUserProjectsJwtId, await this.updateAllFeed();
); }
this.numNewOffersToUserProjects = offersToUserProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any /**
} catch (err: any) { * Loads new offers for user and their projects
logConsoleAndDb("Error retrieving settings or feed: " + err, true); * Updates:
this.$notify( * - Number of new direct offers
{ * - Number of new project offers
group: "alert", * - Rate limit status for both
type: "danger", * @requires Active DID
title: "Error", */
text: private async loadNewOffers() {
err.userMessage || if (this.activeDid) {
"There was an error retrieving your settings or the latest activity.", const offersToUserData = await getNewOffersToUser(
}, this.axios,
5000, 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; * Checks if user needs onboarding
const account = await registerSaveAndActivatePasskey( * Opens onboarding dialog if not completed
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""), */
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() { resultsAreFiltered() {
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby; return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
} }
/**
* Checks if browser notifications are supported
* @returns true if Notification API is available
*/
notificationsSupported() { notificationsSupported() {
return "Notification" in window; 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() { async reloadFeedOnChange() {
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
@ -636,9 +715,9 @@ export default class HomeView extends Vue {
} }
/** /**
* Data loader used by infinite scroller * Loads more feed items for infinite scroll
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload Boolean indicating if more items should be loaded
**/ */
async loadMoreGives(payload: boolean) { async loadMoreGives(payload: boolean) {
// Since feed now loads projects along the way, it takes longer // Since feed now loads projects along the way, it takes longer
// and the InfiniteScroll component triggers a load before finished. // 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() { async updateAllFeed() {
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true; let endOfResults = true;
@ -917,6 +1002,11 @@ export default class HomeView extends Vue {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; 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) { openDialog(giver?: GiverReceiverInputInfo, description?: string) {
(this.$refs.customDialog as GiftedDialog).open( (this.$refs.customDialog as GiftedDialog).open(
giver, giver,
@ -930,12 +1020,20 @@ export default class HomeView extends Vue {
); );
} }
/**
* Opens prompts for gift ideas
* Links to openDialog for selected prompt
*/
openGiftedPrompts() { openGiftedPrompts() {
(this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) => (this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) =>
this.openDialog(giver as GiverReceiverInputInfo, description), this.openDialog(giver as GiverReceiverInputInfo, description),
); );
} }
/**
* Opens feed filter configuration
* @param reloadFeedOnChange Callback for when filters are updated
*/
openFeedFilters() { openFeedFilters() {
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange); (this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
} }

Loading…
Cancel
Save