From 9bdd66b9c93168cd5aa845e15986ce6659f86f33 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 10 Sep 2025 18:19:17 +0800 Subject: [PATCH 1/5] feat(NewActivityView): enhance "See all" links to mark offers as read before navigation - Replace router-links with click handlers for both "See all" offers links - Add handleSeeAllOffersToUser and handleSeeAllOffersToUserProjects methods - Modify expandOffersToUserAndMarkRead to accept fromSeeAll parameter for contextual notifications - Modify expandOffersToUserProjectsAndMarkRead to accept fromSeeAll parameter for contextual notifications - Show shorter notification messages when called from "See all" vs chevron expand buttons - Add safety checks to prevent errors when offers arrays are empty - Standardize notification message text consistency - TypeScript and formatting lint fixes Both "See all" links now properly mark offers as viewed before navigation, preventing users from seeing unread offers in the detailed views. --- src/libs/util.ts | 17 +++++++++----- src/views/ClaimView.vue | 2 +- src/views/ConfirmGiftView.vue | 2 +- src/views/NewActivityView.vue | 42 ++++++++++++++++++++++------------- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/libs/util.ts b/src/libs/util.ts index c64916cc..432d8f72 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -165,18 +165,25 @@ export interface OfferFulfillment { offerType: string; } +interface FulfillmentObject { + "@type": string; + identifier?: string; +} + /** * Extract offer fulfillment information from the fulfills field * Handles both array and single object cases */ -export const extractOfferFulfillment = (fulfills: any): OfferFulfillment | null => { +export const extractOfferFulfillment = ( + fulfills: FulfillmentObject | FulfillmentObject[] | null | undefined, +): OfferFulfillment | null => { if (!fulfills) { return null; } - + // Handle both array and single object cases let offerFulfill = null; - + if (Array.isArray(fulfills)) { // Find the Offer in the fulfills array offerFulfill = fulfills.find((item) => item["@type"] === "Offer"); @@ -184,14 +191,14 @@ export const extractOfferFulfillment = (fulfills: any): OfferFulfillment | null // fulfills is a single Offer object offerFulfill = fulfills; } - + if (offerFulfill) { return { offerHandleId: offerFulfill.identifier, offerType: offerFulfill["@type"], }; } - + return null; }; diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 2c441687..05ba7e07 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -734,7 +734,7 @@ export default class ClaimView extends Vue { */ extractOfferFulfillment() { this.detailsForGiveOfferFulfillment = libsUtil.extractOfferFulfillment( - this.detailsForGive?.fullClaim?.fulfills + this.detailsForGive?.fullClaim?.fulfills, ); } diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 95632bb7..5dc9fe0c 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -719,7 +719,7 @@ export default class ConfirmGiftView extends Vue { */ private extractOfferFulfillment() { this.giveDetailsOfferFulfillment = libsUtil.extractOfferFulfillment( - this.giveDetails?.fullClaim?.fulfills + this.giveDetails?.fullClaim?.fulfills, ); } diff --git a/src/views/NewActivityView.vue b/src/views/NewActivityView.vue index fbcd7423..854d19ab 100644 --- a/src/views/NewActivityView.vue +++ b/src/views/NewActivityView.vue @@ -32,9 +32,9 @@ @click="expandOffersToUserAndMarkRead()" /> - + See all - +
@@ -99,9 +99,9 @@ @click="expandOffersToUserProjectsAndMarkRead()" />
- + See all - +
@@ -239,18 +239,18 @@ export default class NewActivityView extends Vue { } } - async expandOffersToUserAndMarkRead() { + async expandOffersToUserAndMarkRead(fromSeeAll: boolean = false) { this.showOffersDetails = !this.showOffersDetails; - if (this.showOffersDetails) { + if (this.showOffersDetails && this.newOffersToUser.length > 0) { await this.$updateSettings({ lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId, }); // note that we don't update this.lastAckedOfferToUserJwtId in case they // later choose the last one to keep the offers as new - this.notify.info( - "The offers are marked as viewed. Click in the list to keep them as new.", - TIMEOUTS.LONG, - ); + const message = fromSeeAll + ? "The offers are marked as viewed." + : "The offers are marked as viewed. Click in the list to keep them as new."; + this.notify.info(message, TIMEOUTS.LONG); } } @@ -275,20 +275,20 @@ export default class NewActivityView extends Vue { ); } - async expandOffersToUserProjectsAndMarkRead() { + async expandOffersToUserProjectsAndMarkRead(fromSeeAll: boolean = false) { this.showOffersToUserProjectsDetails = !this.showOffersToUserProjectsDetails; - if (this.showOffersToUserProjectsDetails) { + if (this.showOffersToUserProjectsDetails && this.newOffersToUserProjects.length > 0) { await this.$updateSettings({ lastAckedOfferToUserProjectsJwtId: this.newOffersToUserProjects[0].jwtId, }); // note that we don't update this.lastAckedOfferToUserProjectsJwtId in case // they later choose the last one to keep the offers as new - this.notify.info( - "The offers are now marked as viewed. Click in the list to keep them as new.", - TIMEOUTS.LONG, - ); + const message = fromSeeAll + ? "The offers are marked as viewed." + : "The offers are marked as viewed. Click in the list to keep them as new."; + this.notify.info(message, TIMEOUTS.LONG); } } @@ -314,5 +314,15 @@ export default class NewActivityView extends Vue { TIMEOUTS.STANDARD, ); } + + async handleSeeAllOffersToUser() { + await this.expandOffersToUserAndMarkRead(true); + this.$router.push("/recent-offers-to-user"); + } + + async handleSeeAllOffersToUserProjects() { + await this.expandOffersToUserProjectsAndMarkRead(true); + this.$router.push("/recent-offers-to-user-projects"); + } } From ac603f66e2bf6b4db8dbe64a290560cf71065eb5 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 10 Sep 2025 18:19:40 +0800 Subject: [PATCH 2/5] Lint fix --- src/views/NewActivityView.vue | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/views/NewActivityView.vue b/src/views/NewActivityView.vue index 854d19ab..a37ca36a 100644 --- a/src/views/NewActivityView.vue +++ b/src/views/NewActivityView.vue @@ -32,7 +32,7 @@ @click="expandOffersToUserAndMarkRead()" />
- + See all @@ -99,7 +99,10 @@ @click="expandOffersToUserProjectsAndMarkRead()" /> - + See all @@ -247,7 +250,7 @@ export default class NewActivityView extends Vue { }); // note that we don't update this.lastAckedOfferToUserJwtId in case they // later choose the last one to keep the offers as new - const message = fromSeeAll + const message = fromSeeAll ? "The offers are marked as viewed." : "The offers are marked as viewed. Click in the list to keep them as new."; this.notify.info(message, TIMEOUTS.LONG); @@ -278,14 +281,17 @@ export default class NewActivityView extends Vue { async expandOffersToUserProjectsAndMarkRead(fromSeeAll: boolean = false) { this.showOffersToUserProjectsDetails = !this.showOffersToUserProjectsDetails; - if (this.showOffersToUserProjectsDetails && this.newOffersToUserProjects.length > 0) { + if ( + this.showOffersToUserProjectsDetails && + this.newOffersToUserProjects.length > 0 + ) { await this.$updateSettings({ lastAckedOfferToUserProjectsJwtId: this.newOffersToUserProjects[0].jwtId, }); // note that we don't update this.lastAckedOfferToUserProjectsJwtId in case // they later choose the last one to keep the offers as new - const message = fromSeeAll + const message = fromSeeAll ? "The offers are marked as viewed." : "The offers are marked as viewed. Click in the list to keep them as new."; this.notify.info(message, TIMEOUTS.LONG); From 5d9f455fc8744ddb3c325d899d8b0f664c2dabc0 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 16 Sep 2025 18:10:17 +0800 Subject: [PATCH 3/5] feat: move mark-as-read logic from navigation to view loading - Remove mark-as-read logic from NewActivityView navigation handlers - Add mark-as-read logic to RecentOffersToUserView and RecentOffersToUserProjectsView after data loading - Improve "You've already seen all the following" marker positioning - Update marker styling with dashed border and centered text This ensures the marker appears at the correct position in the list instead of always at the top, providing better UX when viewing offers. --- src/views/NewActivityView.vue | 2 -- src/views/RecentOffersToUserProjectsView.vue | 21 ++++++++++++++------ src/views/RecentOffersToUserView.vue | 20 +++++++++++++------ 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/views/NewActivityView.vue b/src/views/NewActivityView.vue index a37ca36a..233a8f4d 100644 --- a/src/views/NewActivityView.vue +++ b/src/views/NewActivityView.vue @@ -322,12 +322,10 @@ export default class NewActivityView extends Vue { } async handleSeeAllOffersToUser() { - await this.expandOffersToUserAndMarkRead(true); this.$router.push("/recent-offers-to-user"); } async handleSeeAllOffersToUserProjects() { - await this.expandOffersToUserProjectsAndMarkRead(true); this.$router.push("/recent-offers-to-user-projects"); } } diff --git a/src/views/RecentOffersToUserProjectsView.vue b/src/views/RecentOffersToUserProjectsView.vue index c294d053..4125f983 100644 --- a/src/views/RecentOffersToUserProjectsView.vue +++ b/src/views/RecentOffersToUserProjectsView.vue @@ -32,20 +32,20 @@ -
    +
    • +
      - You've already seen all the following + + You've already seen all the following +
      {{ @@ -142,6 +142,15 @@ export default class RecentOffersToUserView extends Vue { this.newOffersToUserProjects = offersToUserProjectsData.data; this.newOffersToUserProjectsAtEnd = !offersToUserProjectsData.hitLimit; + // Mark offers as read after data is loaded + if (this.newOffersToUserProjects.length > 0) { + await this.$updateSettings({ + lastAckedOfferToUserProjectsJwtId: + this.newOffersToUserProjects[0].jwtId, + }); + this.notify.info("The offers are marked as viewed.", TIMEOUTS.LONG); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { logger.error("Error retrieving settings & contacts:", err); diff --git a/src/views/RecentOffersToUserView.vue b/src/views/RecentOffersToUserView.vue index 5996f7ce..b6bc5809 100644 --- a/src/views/RecentOffersToUserView.vue +++ b/src/views/RecentOffersToUserView.vue @@ -27,20 +27,20 @@

      -
        +
        • +
          - You've already seen all the following + + You've already seen all the following +
          {{ @@ -133,6 +133,14 @@ export default class RecentOffersToUserView extends Vue { this.newOffersToUser = offersToUserData.data; this.newOffersToUserAtEnd = !offersToUserData.hitLimit; + // Mark offers as read after data is loaded + if (this.newOffersToUser.length > 0) { + await this.$updateSettings({ + lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId, + }); + this.notify.info("The offers are marked as viewed.", TIMEOUTS.LONG); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { logger.error("Error retrieving settings & contacts:", err); From b1fcb49e7cc406dddc3e36e26fbdf942362e68f1 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Thu, 18 Sep 2025 21:42:15 +0800 Subject: [PATCH 4/5] fix: initialize notification helpers in lifecycle methods - Fix 't is not a function' error during image upload by properly initializing notification helpers - Move notification helper initialization from class-level to lifecycle methods (created/mounted) - Affected components: ImageMethodDialog, SeedBackupView, QuickActionBvcBeginView, HelpNotificationsView - Ensures $notify is available when createNotifyHelpers() is called - Resolves notification errors in image upload functionality --- src/components/ImageMethodDialog.vue | 5 ++++- src/views/HelpNotificationsView.vue | 5 ++++- src/views/QuickActionBvcBeginView.vue | 5 ++++- src/views/SeedBackupView.vue | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/ImageMethodDialog.vue b/src/components/ImageMethodDialog.vue index 5cb99094..74c822a8 100644 --- a/src/components/ImageMethodDialog.vue +++ b/src/components/ImageMethodDialog.vue @@ -293,7 +293,7 @@ const inputImageFileNameRef = ref(); export default class ImageMethodDialog extends Vue { $notify!: NotifyFunction; $router!: Router; - notify = createNotifyHelpers(this.$notify); + notify!: ReturnType; /** Active DID for user authentication */ activeDid = ""; @@ -498,6 +498,9 @@ export default class ImageMethodDialog extends Vue { * @throws {Error} When settings retrieval fails */ async mounted() { + // Initialize notification helpers + this.notify = createNotifyHelpers(this.$notify); + try { // Get activeDid from active_identity table (single source of truth) // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/views/HelpNotificationsView.vue b/src/views/HelpNotificationsView.vue index 81f45f1f..e768c870 100644 --- a/src/views/HelpNotificationsView.vue +++ b/src/views/HelpNotificationsView.vue @@ -394,7 +394,7 @@ export default class HelpNotificationsView extends Vue { notifyingReminderTime = ""; // Notification helper system - notify = createNotifyHelpers(this.$notify); + notify!: ReturnType; /** * Computed property for consistent button styling @@ -430,6 +430,9 @@ export default class HelpNotificationsView extends Vue { * Handles errors gracefully with proper logging without exposing sensitive data. */ async mounted() { + // Initialize notification helpers + this.notify = createNotifyHelpers(this.$notify); + try { const registration = await navigator.serviceWorker?.ready; const fullSub = await registration?.pushManager.getSubscription(); diff --git a/src/views/QuickActionBvcBeginView.vue b/src/views/QuickActionBvcBeginView.vue index 4293cc1c..578f33fb 100644 --- a/src/views/QuickActionBvcBeginView.vue +++ b/src/views/QuickActionBvcBeginView.vue @@ -99,7 +99,7 @@ export default class QuickActionBvcBeginView extends Vue { $router!: Router; // Notification helper system - private notify = createNotifyHelpers(this.$notify); + private notify!: ReturnType; attended = true; gaveTime = true; @@ -111,6 +111,9 @@ export default class QuickActionBvcBeginView extends Vue { * Uses America/Denver timezone for Bountiful location */ async mounted() { + // Initialize notification helpers + this.notify = createNotifyHelpers(this.$notify); + logger.debug( "[QuickActionBvcBeginView] Mounted - calculating meeting date", ); diff --git a/src/views/SeedBackupView.vue b/src/views/SeedBackupView.vue index 8fb4e0a6..bdbe3545 100644 --- a/src/views/SeedBackupView.vue +++ b/src/views/SeedBackupView.vue @@ -162,7 +162,7 @@ export default class SeedBackupView extends Vue { showSeed = false; // Notification helper system - notify = createNotifyHelpers(this.$notify); + notify!: ReturnType; /** * Computed property for consistent copy feedback styling @@ -204,6 +204,9 @@ export default class SeedBackupView extends Vue { * Handles errors gracefully with user notifications. */ async created() { + // Initialize notification helpers + this.notify = createNotifyHelpers(this.$notify); + try { let activeDid = ""; From 6fd53b020e06e2f522dd43f5e021b4cf6d9a6695 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Fri, 19 Sep 2025 15:00:17 +0800 Subject: [PATCH 5/5] refactor: simplify notification messages for offer viewing - Remove conditional notification logic in NewActivityView - Remove redundant notification in RecentOffersToUserView and RecentOffersToUserProjectsView - Standardize to single notification message format --- src/views/NewActivityView.vue | 20 ++++++++++---------- src/views/RecentOffersToUserProjectsView.vue | 1 - src/views/RecentOffersToUserView.vue | 1 - 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/views/NewActivityView.vue b/src/views/NewActivityView.vue index 233a8f4d..ede134ae 100644 --- a/src/views/NewActivityView.vue +++ b/src/views/NewActivityView.vue @@ -242,7 +242,7 @@ export default class NewActivityView extends Vue { } } - async expandOffersToUserAndMarkRead(fromSeeAll: boolean = false) { + async expandOffersToUserAndMarkRead() { this.showOffersDetails = !this.showOffersDetails; if (this.showOffersDetails && this.newOffersToUser.length > 0) { await this.$updateSettings({ @@ -250,10 +250,10 @@ export default class NewActivityView extends Vue { }); // note that we don't update this.lastAckedOfferToUserJwtId in case they // later choose the last one to keep the offers as new - const message = fromSeeAll - ? "The offers are marked as viewed." - : "The offers are marked as viewed. Click in the list to keep them as new."; - this.notify.info(message, TIMEOUTS.LONG); + this.notify.info( + "The offers are marked as viewed. Click in the list to keep them as new.", + TIMEOUTS.LONG, + ); } } @@ -278,7 +278,7 @@ export default class NewActivityView extends Vue { ); } - async expandOffersToUserProjectsAndMarkRead(fromSeeAll: boolean = false) { + async expandOffersToUserProjectsAndMarkRead() { this.showOffersToUserProjectsDetails = !this.showOffersToUserProjectsDetails; if ( @@ -291,10 +291,10 @@ export default class NewActivityView extends Vue { }); // note that we don't update this.lastAckedOfferToUserProjectsJwtId in case // they later choose the last one to keep the offers as new - const message = fromSeeAll - ? "The offers are marked as viewed." - : "The offers are marked as viewed. Click in the list to keep them as new."; - this.notify.info(message, TIMEOUTS.LONG); + this.notify.info( + "The offers are marked as viewed. Click in the list to keep them as new.", + TIMEOUTS.LONG, + ); } } diff --git a/src/views/RecentOffersToUserProjectsView.vue b/src/views/RecentOffersToUserProjectsView.vue index 4125f983..987c05b8 100644 --- a/src/views/RecentOffersToUserProjectsView.vue +++ b/src/views/RecentOffersToUserProjectsView.vue @@ -148,7 +148,6 @@ export default class RecentOffersToUserView extends Vue { lastAckedOfferToUserProjectsJwtId: this.newOffersToUserProjects[0].jwtId, }); - this.notify.info("The offers are marked as viewed.", TIMEOUTS.LONG); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/views/RecentOffersToUserView.vue b/src/views/RecentOffersToUserView.vue index b6bc5809..ac0c42c9 100644 --- a/src/views/RecentOffersToUserView.vue +++ b/src/views/RecentOffersToUserView.vue @@ -138,7 +138,6 @@ export default class RecentOffersToUserView extends Vue { await this.$updateSettings({ lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId, }); - this.notify.info("The offers are marked as viewed.", TIMEOUTS.LONG); } // eslint-disable-next-line @typescript-eslint/no-explicit-any