diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index 22832b6bf..8766381cd 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -32,7 +32,8 @@ export type Settings = { imageServer?: string; lastName?: string; // deprecated - put all names in firstName - lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged + lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing + lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing // The claim list has a most recent one used in notifications that's separate from the last viewed lastNotifiedClaimId?: string; diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 82025b9f8..eb5d57e15 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -112,6 +112,10 @@ export interface OfferSummaryRecord { validThrough: string; } +export interface OfferToPlanSummaryRecord extends OfferSummaryRecord { + planName: string; +} + // a summary record; the VC is not currently part of this record export interface PlanSummaryRecord { agentDid?: string; // optional, if the issuer wants someone else to manage as well @@ -603,6 +607,22 @@ export async function getNewOffersToUser( return offers; } +export async function getNewOffersToUserProjects( + axios: Axios, + apiServer: string, + activeDid: string, + lastAckedOfferToUserProjectsJwtId?: string, +) { + let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`; + if (lastAckedOfferToUserProjectsJwtId) { + url += "?afterId=" + lastAckedOfferToUserProjectsJwtId; + } + const headers = await getHeaders(activeDid); + const response = await axios.get(url, { headers }); + const offers = response.data.data; + return offers; +} + /** * Construct GiveAction VC for submission to server * diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 95652a2c5..783eb348d 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -197,14 +197,14 @@

new offer{{ numNewOffersToUser === 1 ? "" : "s" }} to you

+
+ + {{ numNewOffersToUserProjects }} + +

+ new offer{{ numNewOffersToUserProjects === 1 ? "" : "s" }} to your + projects +

+
@@ -379,6 +394,7 @@ import { fetchEndorserRateLimits, getHeaders, getNewOffersToUser, + getNewOffersToUserProjects, getPlanFromCache, GiveSummaryRecord, } from "@/libs/endorserServer"; @@ -443,8 +459,10 @@ export default class HomeView extends Vue { isFeedFilteredByNearby = false; isFeedLoading = true; isRegistered = false; - lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged + lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing + lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing numNewOffersToUser: number = 0; // number of new offers-to-user + numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects searchBoxes: Array<{ name: string; bbox: BoundingBox; @@ -475,6 +493,8 @@ export default class HomeView extends Vue { this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; this.isRegistered = !!settings.isRegistered; this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; + this.lastAckedOfferToUserProjectsJwtId = + settings.lastAckedOfferToUserProjectsJwtId; this.searchBoxes = settings.searchBoxes || []; this.showShortcutBvc = !!settings.showShortcutBvc; @@ -519,6 +539,17 @@ export default class HomeView extends Vue { ).length; } + if (this.activeDid) { + this.numNewOffersToUserProjects = ( + await getNewOffersToUserProjects( + this.axios, + this.apiServer, + this.activeDid, + this.lastAckedOfferToUserProjectsJwtId, + ) + ).length; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { console.error("Error retrieving settings or feed.", err); diff --git a/src/views/NewActivityView.vue b/src/views/NewActivityView.vue index 9f3ecc6e4..614852543 100644 --- a/src/views/NewActivityView.vue +++ b/src/views/NewActivityView.vue @@ -16,12 +16,13 @@
-
+
{{ newOffersToUser.length }} New Offer{{ newOffersToUser.length === 1 ? "" : "s" }} To You
-
+
  • {{ @@ -64,6 +65,64 @@
+ + +
+ {{ + newOffersToUserProjects.length + }} + New Offer{{ newOffersToUserProjects.length === 1 ? "" : "s" }} To Your + Projects + +
+ +
+
    +
  • + {{ + didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts) + }} + offers + {{ + offer.objectDescription + }}{{ offer.objectDescription && offer.amount ? ", and " : "" }} + {{ + displayAmount(offer.unit, offer.amount) + }} + to + {{ offer.planName }} + + + + + +
  • +
+
@@ -85,7 +144,9 @@ import { didInfo, displayAmount, getNewOffersToUser, + getNewOffersToUserProjects, OfferSummaryRecord, + OfferToPlanSummaryRecord, } from "@/libs/endorserServer"; @Component({ @@ -99,10 +160,12 @@ export default class NewActivityView extends Vue { allMyDids: string[] = []; apiServer = ""; lastAckedOfferToUserJwtId = ""; + lastAckedOfferToUserProjectsJwtId = ""; newOffersToUser: Array = []; + newOffersToUserProjects: Array = []; showOffersDetails = false; - + showOffersToUserProjectsDetails = false; didInfo = didInfo; displayAmount = displayAmount; @@ -112,6 +175,8 @@ export default class NewActivityView extends Vue { this.apiServer = settings.apiServer || ""; this.activeDid = settings.activeDid || ""; this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || ""; + this.lastAckedOfferToUserProjectsJwtId = + settings.lastAckedOfferToUserProjectsJwtId || ""; this.allContacts = await db.contacts.toArray(); @@ -126,6 +191,12 @@ export default class NewActivityView extends Vue { this.activeDid, this.lastAckedOfferToUserJwtId, ); + this.newOffersToUserProjects = await getNewOffersToUserProjects( + this.axios, + this.apiServer, + this.activeDid, + this.lastAckedOfferToUserProjectsJwtId, + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { @@ -144,37 +215,87 @@ export default class NewActivityView extends Vue { async expandOffersToUserAndMarkRead() { this.showOffersDetails = !this.showOffersDetails; - if (this.newOffersToUser.length > 0) { + if (this.showOffersDetails) { await updateAccountSettings(this.activeDid, { 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( + { + group: "alert", + type: "info", + title: "Marked as Read", + text: "The offers are marked as viewed. Click in the list to keep them as new.", + }, + 5000, + ); + } + } + + async markOffersAsReadStartingWith(jwtId: string) { + const index = this.newOffersToUser.findIndex( + (offer) => offer.jwtId === jwtId, + ); + if (index !== -1 && index < this.newOffersToUser.length - 1) { + // Set to the next offer's jwtId + await updateAccountSettings(this.activeDid, { + lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId, + }); + } else { + // it's the last entry (or not found), so just keep it the same + await updateAccountSettings(this.activeDid, { + lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId, + }); } this.$notify( { group: "alert", type: "info", - title: "Marked as Read", - text: "The offers are marked as viewed. Click in the list to keep them as new.", + title: "Marked as Unread", + text: "All offers above that one are marked as unread.", }, - 5000, + 3000, ); } - async markOffersAsReadStartingWith(jwtId: string) { - const index = this.newOffersToUser.findIndex( + async expandOffersToUserProjectsAndMarkRead() { + this.showOffersToUserProjectsDetails = + !this.showOffersToUserProjectsDetails; + if (this.showOffersToUserProjectsDetails) { + await updateAccountSettings(this.activeDid, { + 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( + { + group: "alert", + type: "info", + title: "Marked as Read", + text: "The offers are marked as viewed. Click in the list to keep them as new.", + }, + 5000, + ); + } + } + + async markOffersToUserProjectsAsReadStartingWith(jwtId: string) { + const index = this.newOffersToUserProjects.findIndex( (offer) => offer.jwtId === jwtId, ); - if (index !== -1 && index < this.newOffersToUser.length - 1) { + if (index !== -1 && index < this.newOffersToUserProjects.length - 1) { // Set to the next offer's jwtId await updateAccountSettings(this.activeDid, { - lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId, + lastAckedOfferToUserProjectsJwtId: + this.newOffersToUserProjects[index + 1].jwtId, }); } else { // it's the last entry (or not found), so just keep it the same await updateAccountSettings(this.activeDid, { - lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId, + lastAckedOfferToUserProjectsJwtId: + this.lastAckedOfferToUserProjectsJwtId, }); } this.$notify( diff --git a/test-playwright/60-new-activity.spec.ts b/test-playwright/60-new-activity.spec.ts index 86e05551e..26e18cb4a 100644 --- a/test-playwright/60-new-activity.spec.ts +++ b/test-playwright/60-new-activity.spec.ts @@ -47,7 +47,7 @@ test('New offers for another user', async ({ page }) => { await expect(offerNumElem).toHaveText('2'); await offerNumElem.click(); - await expect(page.getByText('New Offers To You')).toBeVisible(); + await expect(page.getByText('New Offers To You', { exact: true })).toBeVisible(); await page.getByTestId('showOffersToUser').click(); // note that they show in reverse chronologicalorder await expect(page.getByText(`help of ${randomString2} from #000`)).toBeVisible(); @@ -68,7 +68,7 @@ test('New offers for another user', async ({ page }) => { offerNumElem = page.getByTestId('newDirectOffersActivityNumber'); await expect(offerNumElem).toHaveText('1'); await offerNumElem.click(); - await expect(page.getByText('New Offer To You')).toBeVisible(); + await expect(page.getByText('New Offer To You', { exact: true })).toBeVisible(); await page.getByTestId('showOffersToUser').click(); // now see that no offers are shown as new