From 5217ee7bf6652de43214f2aafea5bd31cbb52c20 Mon Sep 17 00:00:00 2001 From: Trent Larson <trent@trentlarson.com> Date: Thu, 28 Nov 2024 11:26:51 -0700 Subject: [PATCH] refactor project screen: add action to record a give from it, and add checks to give confirmation buttons --- src/components/GiftedDialog.vue | 10 +- src/libs/util.ts | 153 +++++++++++++- src/views/ClaimView.vue | 56 +++--- src/views/ConfirmGiftView.vue | 53 +++-- src/views/ContactGiftingView.vue | 2 +- src/views/GiftedDetailsView.vue | 2 +- src/views/OfferDetailsView.vue | 2 +- src/views/ProjectViewView.vue | 330 +++++++++++++++++++++---------- 8 files changed, 428 insertions(+), 180 deletions(-) diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 91352cbf3..4000c82dc 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -47,7 +47,8 @@ giverDid: giver?.did, giverName: giver?.name, offerId, - fulfillsProjectId: projectId, + fulfillsProjectId: toProjectId, + providerProjectId: fromProjectId, recipientDid: receiver?.did, recipientName: receiver?.name, unitCode, @@ -98,7 +99,8 @@ import { Contact } from "@/db/tables/contacts"; export default class GiftedDialog extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; - @Prop projectId = ""; + @Prop fromProjectId = ""; + @Prop toProjectId = ""; activeDid = ""; allContacts: Array<Contact> = []; @@ -294,9 +296,11 @@ export default class GiftedDialog extends Vue { description, amount, unitCode, - this.projectId, + this.toProjectId, this.offerId, this.isTrade, + undefined, + this.fromProjectId, ); if ( diff --git a/src/libs/util.ts b/src/libs/util.ts index 2b0d85590..c11f046d5 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -5,7 +5,7 @@ import { Buffer } from "buffer"; import * as R from "ramda"; import { useClipboard } from "@vueuse/core"; -import { DEFAULT_PUSH_SERVER } from "@/constants/app"; +import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app"; import { accountsDB, retrieveSettingsForActiveAccount, @@ -21,6 +21,7 @@ import { containsHiddenDid, GenericCredWrapper, GenericVerifiableCredential, + GiveSummaryRecord, OfferVerifiableCredential, } from "@/libs/endorserServer"; import { KeyMeta } from "@/libs/crypto/vc"; @@ -101,10 +102,14 @@ export const isGlobalUri = (uri: string) => { return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); }; +export const isGiveClaimType = (claimType?: string) => { + return claimType === "GiveAction"; +}; + export const isGiveAction = ( veriClaim: GenericCredWrapper<GenericVerifiableCredential>, ) => { - return veriClaim.claimType === "GiveAction"; + return isGiveClaimType(veriClaim.claimType); }; export const nameForDid = ( @@ -136,16 +141,75 @@ export const doCopyTwoSecRedo = (text: string, fn: () => void) => { .then(() => setTimeout(fn, 2000)); }; +export interface ConfirmerData { + confirmerIdList: string[]; + confsVisibleToIdList: string[]; + numConfsNotVisible: number; +} + +/** + * @return only confirmers, excluding the issuer and hidden DIDs + */ +export async function retrieveConfirmerIdList( + apiServer: string, + claimId: string, + claimIssuerId: string, + userDid: string, +): Promise<ConfirmerData | undefined> { + const confirmUrl = + apiServer + + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" + + encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); + const confirmHeaders = await serverUtil.getHeaders(userDid); + const response = await axios.get(confirmUrl, { + headers: confirmHeaders, + }); + if (response.status === 200) { + const resultList1 = response.data.result || []; + //const publicUrls = resultList.publicUrls || []; + delete resultList1.publicUrls; + // exclude hidden DIDs + const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); + // exclude the issuer + const resultList3 = R.reject( + (did: string) => did === claimIssuerId, + resultList2, + ); + const confirmerIdList = resultList3; + let numConfsNotVisible = resultList1.length - resultList2.length; + if (resultList3.length === resultList2.length) { + // the issuer was not in the "visible" list so they must be hidden + // so subtract them from the non-visible confirmers count + numConfsNotVisible = numConfsNotVisible - 1; + } + const confsVisibleToIdList = response.data.result.resultVisibleToDids || []; + const result: ConfirmerData = { + confirmerIdList, + confsVisibleToIdList, + numConfsNotVisible, + }; + return result; + } else { + console.error( + "Bad response status of", + response.status, + "for confirmers:", + response, + ); + return undefined; + } +} + /** * @returns true if the user can confirm the claim * @param veriClaim is expected to have fields: claim, claimType, and issuer */ -export const isGiveRecordTheUserCanConfirm = ( +export function isGiveRecordTheUserCanConfirm( isRegistered: boolean, veriClaim: GenericCredWrapper<GenericVerifiableCredential>, activeDid: string, confirmerIdList: string[] = [], -) => { +): boolean { return ( isRegistered && isGiveAction(veriClaim) && @@ -153,7 +217,78 @@ export const isGiveRecordTheUserCanConfirm = ( veriClaim.issuer !== activeDid && !containsHiddenDid(veriClaim.claim) ); -}; +} + +export function notifyWhyCannotConfirm( + notifyFun: (notification: NotificationIface, timeout: number) => void, + isRegistered: boolean, + claimType: string | undefined, + giveDetails: GiveSummaryRecord | undefined, + activeDid: string, + confirmerIdList: string[] = [], +) { + if (!isRegistered) { + notifyFun( + { + group: "alert", + type: "info", + title: "Not Registered", + text: "Someone needs to register you before you can confirm.", + }, + 3000, + ); + } else if (!isGiveClaimType(claimType)) { + notifyFun( + { + group: "alert", + type: "info", + title: "Not A Give", + text: "This is not a giving action to confirm.", + }, + 3000, + ); + } else if (confirmerIdList.includes(activeDid)) { + notifyFun( + { + group: "alert", + type: "info", + title: "Already Confirmed", + text: "You already confirmed this claim.", + }, + 3000, + ); + } else if (giveDetails?.issuerDid == activeDid) { + notifyFun( + { + group: "alert", + type: "info", + title: "Cannot Confirm", + text: "You cannot confirm this because you issued this claim.", + }, + 3000, + ); + } else if (serverUtil.containsHiddenDid(giveDetails?.fullClaim)) { + notifyFun( + { + group: "alert", + type: "info", + title: "Cannot Confirm", + text: "You cannot confirm this because some people are hidden.", + }, + 3000, + ); + } else { + notifyFun( + { + group: "alert", + type: "info", + title: "Cannot Confirm", + text: "You cannot confirm this claim. There are no other details -- we can help more if you contact us and send us screenshots.", + }, + 3000, + ); + } +} export async function blobToBase64(blob: Blob): Promise<string> { return new Promise((resolve, reject) => { @@ -191,9 +326,9 @@ export function base64ToBlob(base64DataUrl: string, sliceSize = 512) { * @returns the DID of the person who offered, or undefined if hidden * @param veriClaim is expected to have fields: claim and issuer */ -export const offerGiverDid: ( - arg0: GenericCredWrapper<OfferVerifiableCredential>, -) => string | undefined = (veriClaim) => { +export function offerGiverDid( + veriClaim: GenericCredWrapper<OfferVerifiableCredential>, +): string | undefined { let giver; if ( veriClaim.claim.offeredBy?.identifier && @@ -204,7 +339,7 @@ export const offerGiverDid: ( giver = veriClaim.issuer; } return giver; -}; +} /** * @returns true if the user can fulfill the offer diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 0e48473d9..bb615d62f 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -24,9 +24,11 @@ {{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }} <button v-if=" - ['GiveAction', 'Offer'].includes( + ['GiveAction', 'Offer', 'PlanAction'].includes( veriClaim.claimType as string, ) && veriClaim.issuer === activeDid + // a PlanAction agent also could edit one of those, but rather than add more Plan-specific logic to detect the agent + // we'll let them click the Project link and edit from there " @click="onClickEditClaim" title="Edit" @@ -150,6 +152,10 @@ </div> </div> </div> + <div class="mt-2"> + <fa icon="comment" class="text-slate-400" /> + {{ issuerName }} posted that. + </div> <div class="mt-8"> <button @@ -217,7 +223,7 @@ Nobody that you know has issued or confirmed this claim. </div> <div v-if="confirmerIdList.length > 0"> - The following people have issued or confirmed this claim. + The following people have confirmed this claim. <ul class="ml-4"> <li v-for="confirmerId in confirmerIdList" @@ -503,6 +509,7 @@ export default class ClaimView extends Vue { fullClaimMessage = ""; isEditedGlobalId = false; isRegistered = false; + issuerName = ""; numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible providersForGive: ProviderInfo[] = []; showIdCopy = false; @@ -545,6 +552,7 @@ export default class ClaimView extends Vue { const accounts = accountsDB.accounts; const accountsArr: Array<Account> = await accounts?.toArray(); this.allMyDids = accountsArr.map((acc) => acc.did); + this.issuerName = this.didInfo(this.veriClaim.issuer); const pathParam = window.location.pathname.substring("/claim/".length); let claimId; @@ -696,32 +704,16 @@ export default class ClaimView extends Vue { } // retrieve the list of confirmers - const confirmUrl = - this.apiServer + - "/api/report/issuersWhoClaimedOrConfirmed?claimId=" + - encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); - const confirmHeaders = await serverUtil.getHeaders(userDid); - const response = await this.axios.get(confirmUrl, { - headers: confirmHeaders, - }); - if (response.status === 200) { - const resultList1 = response.data.result || []; - //const publicUrls = resultList.publicUrls || []; - delete resultList1.publicUrls; - const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); - const resultList3 = R.reject( - (did: string) => did === this.veriClaim.issuer, - resultList2, - ); - this.confirmerIdList = resultList3; - this.numConfsNotVisible = resultList1.length - resultList2.length; - if (resultList3.length === resultList2.length) { - // the issuer was not in the "visible" list so they must be hidden - // so subtract them from the non-visible confirmers count - this.numConfsNotVisible = this.numConfsNotVisible - 1; - } - this.confsVisibleToIdList = - response.data.result.resultVisibleToDids || []; + const confirmerInfo = await libsUtil.retrieveConfirmerIdList( + this.apiServer, + claimId, + this.veriClaim.issuer, + userDid, + ); + if (confirmerInfo) { + this.confirmerIdList = confirmerInfo.confirmerIdList; + this.confsVisibleToIdList = confirmerInfo.confsVisibleToIdList; + this.numConfsNotVisible = confirmerInfo.numConfsNotVisible; } else { this.confsVisibleErrorMessage = "Had problems retrieving confirmations."; @@ -736,7 +728,7 @@ export default class ClaimView extends Vue { title: "Error", text: "Something went wrong retrieving claim data.", }, - -1, + 3000, ); } } @@ -921,6 +913,12 @@ export default class ClaimView extends Vue { }, }; (this.$router as Router).push(route); + } else if (this.veriClaim.claimType === "PlanAction") { + const route = { + name: "new-edit-project", + query: { projectId: this.veriClaim.handleId }, + }; + (this.$router as Router).push(route); } else { console.error( "Unrecognized claim type for edit:", diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 1de591955..737a30a2e 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -172,7 +172,7 @@ Nobody that you know issued or confirmed this claim. </div> <div v-if="confirmerIdList.length > 0"> - The following people issued or confirmed this claim. + The following people confirmed this claim. <ul class="ml-4"> <li v-for="confirmerId in confirmerIdList" @@ -661,34 +661,16 @@ export default class ClaimView extends Vue { } // retrieve the list of confirmers - const confirmUrl = - this.apiServer + - "/api/report/issuersWhoClaimedOrConfirmed?claimId=" + - encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); - const confirmHeaders = await serverUtil.getHeaders(userDid); - const response = await this.axios.get(confirmUrl, { - headers: confirmHeaders, - }); - if (response.status === 200) { - const resultList1 = response.data.result || []; - //const publicUrls = resultList.publicUrls || []; - delete resultList1.publicUrls; - // remove any hidden DIDs - const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); - // remove confirmations by this user - const resultList3 = R.reject( - (did: string) => did === this.giveDetails?.issuerDid, - resultList2, - ); - this.confirmerIdList = resultList3; - this.numConfsNotVisible = resultList1.length - resultList2.length; - if (resultList3.length === resultList2.length) { - // the issuer was not in the "visible" list so they must be hidden - // so subtract them from the non-visible confirmers count - this.numConfsNotVisible = this.numConfsNotVisible - 1; - } - this.confsVisibleToIdList = - response.data.result.resultVisibleToDids || []; + const confirmerInfo = await libsUtil.retrieveConfirmerIdList( + this.apiServer, + claimId, + this.veriClaim.issuer, + userDid, + ); + if (confirmerInfo) { + this.confirmerIdList = confirmerInfo.confirmerIdList; + this.confsVisibleToIdList = confirmerInfo.confsVisibleToIdList; + this.numConfsNotVisible = confirmerInfo.numConfsNotVisible; } else { this.confsVisibleErrorMessage = "Had problems retrieving confirmations."; @@ -797,6 +779,17 @@ export default class ClaimView extends Vue { } notifyWhyCannotConfirm() { + libsUtil.notifyWhyCannotConfirm( + this.$notify, + this.isRegistered, + this.veriClaim.claimType, + this.giveDetails, + this.activeDid, + this.confirmerIdList, + ); + } + + notifyWhyCannotConfirmBak() { if (!this.isRegistered) { this.$notify( { @@ -853,7 +846,7 @@ export default class ClaimView extends Vue { group: "alert", type: "info", title: "Cannot Confirm", - text: "You cannot confirm this claim.", + text: "You cannot confirm this claim. There are no other details, but we can help more if you contact us and send us screenshots.", }, 3000, ); diff --git a/src/views/ContactGiftingView.vue b/src/views/ContactGiftingView.vue index 07a00ac20..f5184dc3a 100644 --- a/src/views/ContactGiftingView.vue +++ b/src/views/ContactGiftingView.vue @@ -65,7 +65,7 @@ </li> </ul> - <GiftedDialog ref="customDialog" :projectId="projectId" /> + <GiftedDialog ref="customDialog" :toProjectId="projectId" /> </section> </template> diff --git a/src/views/GiftedDetailsView.vue b/src/views/GiftedDetailsView.vue index 51ccb5b80..01713b7d5 100644 --- a/src/views/GiftedDetailsView.vue +++ b/src/views/GiftedDetailsView.vue @@ -39,7 +39,7 @@ ? fulfillsProjectName : givenToRecipient ? recipientName - : "someone unidentified" + : "someone not named" }}</span > </h1> diff --git a/src/views/OfferDetailsView.vue b/src/views/OfferDetailsView.vue index e919569f4..a161ecddd 100644 --- a/src/views/OfferDetailsView.vue +++ b/src/views/OfferDetailsView.vue @@ -28,7 +28,7 @@ ? projectName : offeredToRecipient ? recipientName - : "someone unidentified" + : "someone not named" }}</span > </h1> diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 745da2e31..27b0ca554 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -15,7 +15,17 @@ <fa icon="chevron-left" class="fa-fw"></fa> </button> Idea - <h2 class="text-xl font-semibold">{{ name }}</h2> + <h2 class="text-xl font-semibold"> + {{ name }} + <button + v-if="activeDid === issuer || activeDid === agentDid" + @click="onEditClick()" + title="Edit" + data-testId="editClaimButton" + > + <fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" /> + </button> + </h2> </h1> </div> @@ -104,15 +114,6 @@ <fa icon="file-lines" class="pl-2 pt-1 text-blue-500" /> </a> </div> - - <button - v-if="activeDid === issuer || activeDid === agentDid" - type="button" - class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" - @click="onEditClick()" - > - Edit - </button> </div> <div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4 mt-4"> @@ -159,31 +160,14 @@ </div> </div> - <div v-if="activeDid && isRegistered" class="mt-4"> - <div class="text-center"> - <button - data-testId="offerButton" - @click="openOfferDialog()" - class="block w-full text-lg font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" - > - Offer (maybe with conditions)... - </button> - </div> - </div> - <OfferDialog - ref="customOfferDialog" - :projectId="this.projectId" - :projectName="this.name" - /> - <div v-if="activeDid && isRegistered"> <div class="text-center"> <p class="mt-2 mt-4 text-center">Record a contribution from:</p> </div> <ul - class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5" + class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2" > - <li @click="openGiftDialog({ name: 'you', did: activeDid })"> + <li @click="openGiftDialogToProject({ name: 'you', did: activeDid })"> <fa icon="hand" class="fa-fw text-blue-500 text-5xl cursor-pointer" /> <h3 class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer" @@ -191,7 +175,7 @@ You </h3> </li> - <li @click="openGiftDialog()"> + <li @click="openGiftDialogToProject()"> <img src="../assets/blank-square.svg" class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer" @@ -203,9 +187,9 @@ </h3> </li> <li - v-for="contact in allContacts.slice(0, 6)" + v-for="contact in allContacts.slice(0, 5)" :key="contact.did" - @click="openGiftDialog(contact)" + @click="openGiftDialogToProject(contact)" > <EntityIcon :contact="contact" @@ -218,27 +202,41 @@ {{ contact.name || "(no name)" }} </h3> </li> + <li> + <span + v-if="allContacts.length >= 5" + @click="onClickAllContactsGifting()" + class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer" + > + ... or someone else... + </span> + </li> </ul> - - <!-- - Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list - (we want to limit the grid count above to 8 or 12 accounts to keep it compact) - --> - <a - v-if="allContacts.length >= 7" - @click="onClickAllContactsGifting()" - class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" - > - Show More Contacts… - </a> - - <GiftedDialog ref="customGiveDialog" :projectId="this.projectId" /> + <GiftedDialog ref="giveDialogToThis" :toProjectId="this.projectId" /> </div> <!-- Offers & Gifts to & from this --> <div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4"> + <!-- First, offers on the left--> <div class="bg-slate-100 px-4 py-3 rounded-md"> - <h3 class="text-sm font-semibold mb-3">Offered To This Idea</h3> + <div v-if="activeDid && isRegistered"> + <div class="text-center"> + <button + data-testId="offerButton" + @click="openOfferDialog()" + class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md" + > + Offer (maybe with conditions)... + </button> + </div> + </div> + <OfferDialog + ref="customOfferDialog" + :projectId="this.projectId" + :projectName="this.name" + /> + + <h3 class="text-lg font-bold mb-3 mt-4">Offered To This Idea</h3> <div v-if="offersToThis.length === 0"> (None yet. Wanna @@ -300,15 +298,27 @@ </div> </div> + <!-- Now, gives TO this project in the middle --> + <!-- (similar to "FROM" gift display below) --> <div class="bg-slate-100 px-4 py-3 rounded-md"> - <h3 class="text-sm font-semibold mb-3">Given To This Idea</h3> + <div v-if="activeDid && isRegistered"> + <div class="text-center"> + <button + @click="openGiftDialogToProject()" + class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md" + > + Given To This... + </button> + </div> + </div> + + <h3 class="text-lg font-bold mb-3 mt-4">Given To This Idea</h3> <div v-if="givesToThis.length === 0"> (None yet. If you've seen something, say something by clicking a contact above.) </div> - <!-- similar to gift display below --> <ul v-else class="text-sm border-t border-slate-300"> <li v-for="give in givesToThis" @@ -346,12 +356,22 @@ <a @click="onClickLoadClaim(give.jwtId)"> <fa icon="file-lines" class="text-blue-500 cursor-pointer" /> </a> + <a - v-if="checkIsConfirmable(give)" - @click="confirmConfirmClaim(give)" + v-if=" + checkIsConfirmable(give) && + !recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId) + " + @click="deepCheckConfirmable(give)" > <fa icon="circle-check" class="text-blue-500 cursor-pointer" /> </a> + <a v-else-if="checkingConfirmationForJwtId === give.jwtId"> + <fa icon="spinner" class="fa-spin-pulse" /> + </a> + <a v-else @click="shallowNotifyWhyCannotConfirm(give)"> + <fa icon="circle-check" class="text-slate-500 cursor-pointer" /> + </a> </div> <div v-if="give.fullClaim.image" class="flex justify-center"> <a :href="give.fullClaim.image" target="_blank"> @@ -365,57 +385,90 @@ </div> </div> - <div class="grid items-start grid-cols-1 gap-4"> - <div - v-if="givesProvidedByThis.length > 0" - class="bg-slate-100 px-4 py-3 rounded-md" - > - <div> - <h3 class="text-sm font-semibold border-b"> - Individuals Getting Contributions From This - </h3> - <!-- similar to gift display above --> - <ul class="text-sm border-t border-slate-300"> - <li - v-for="give in givesProvidedByThis" - :key="give.id" - class="py-1.5 border-b border-slate-300" + <!-- Finally, gives FROM this project on the right --> + <!-- (similar to "TO" gift display above) --> + <div class="bg-slate-100 px-4 py-3 rounded-md"> + <div v-if="activeDid && isRegistered"> + <div class="text-center"> + <button + @click="openGiftDialogFromProject()" + class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md" + > + Given By This... + </button> + </div> + </div> + <GiftedDialog + ref="giveDialogFromThis" + :fromProjectId="this.projectId" + /> + + <h3 class="text-lg font-bold mb-3 mt-4">Benefitted By This</h3> + + <div v-if="givesProvidedByThis.length === 0">(None yet.)</div> + + <ul v-else class="text-sm border-t border-slate-300"> + <li + v-for="give in givesProvidedByThis" + :key="give.id" + class="py-1.5 border-b border-slate-300" + > + <div class="flex justify-between gap-4"> + <span> + {{ + serverUtil.didInfo( + give.recipientDid, + activeDid, + allMyDids, + allContacts, + ) + }} + </span> + <span v-if="give.amount" class="whitespace-nowrap"> + <fa + :icon="libsUtil.iconForUnitCode(give.unit)" + class="fa-fw text-slate-400" + />{{ give.amount }} + </span> + </div> + <div class="text-slate-500"> + <fa icon="calendar" class="fa-fw text-slate-400" /> + {{ give.issuedAt?.substring(0, 10) }} + </div> + <div v-if="give.description" class="text-slate-500"> + <fa icon="comment" class="fa-fw text-slate-400" /> + {{ give.description }} + </div> + <div class="flex justify-between"> + <a @click="onClickLoadClaim(give.jwtId)"> + <fa icon="file-lines" class="text-blue-500 cursor-pointer" /> + </a> + + <a + v-if=" + checkIsConfirmable(give) && + !recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId) + " + @click="deepCheckConfirmable(give)" > - <div class="flex justify-between gap-4"> - <span> - {{ - serverUtil.didInfo( - give.recipientDid, - activeDid, - allMyDids, - allContacts, - ) - }} - </span> - <span v-if="give.amount" class="whitespace-nowrap"> - <fa - :icon="libsUtil.iconForUnitCode(give.unit)" - class="fa-fw text-slate-400" - />{{ give.amount }} - </span> - </div> - <div class="text-slate-500"> - <fa icon="calendar" class="fa-fw text-slate-400" /> - {{ give.issuedAt?.substring(0, 10) }} - </div> - <div v-if="give.description" class="text-slate-500"> - <fa icon="comment" class="fa-fw text-slate-400" /> - {{ give.description }} - </div> - <a @click="onClickLoadClaim(give.jwtId)"> - <fa icon="file-lines" class="text-blue-500 cursor-pointer" /> - </a> - </li> - </ul> - <div v-if="givesProvidedByHitLimit" class="text-center"> - <button @click="loadGivesProvidedBy()">Load More</button> + <fa icon="circle-check" class="text-blue-500 cursor-pointer" /> + </a> + <a v-else-if="checkingConfirmationForJwtId === give.jwtId"> + <fa icon="spinner" class="fa-spin-pulse" /> + </a> + <a v-else @click="shallowNotifyWhyCannotConfirm(give)"> + <fa icon="circle-check" class="text-slate-500 cursor-pointer" /> + </a> </div> - </div> + <div v-if="give.fullClaim.image" class="flex justify-center"> + <a :href="give.fullClaim.image" target="_blank"> + <img :src="give.fullClaim.image" class="h-24 mt-2 rounded-xl" /> + </a> + </div> + </li> + </ul> + <div v-if="givesProvidedByHitLimit" class="text-center"> + <button @click="loadGivesProvidedBy()">Load More</button> </div> </div> </div> @@ -468,6 +521,7 @@ export default class ProjectViewView extends Vue { allMyDids: Array<string> = []; allContacts: Array<Contact> = []; apiServer = ""; + checkingConfirmationForJwtId = ""; description = ""; expanded = false; fulfilledByThis: PlanSummaryRecord | null = null; @@ -486,6 +540,7 @@ export default class ProjectViewView extends Vue { offersToThis: Array<OfferSummaryRecord> = []; offersHitLimit = false; projectId = ""; // handle ID + recentlyCheckedAndUnconfirmableJwts = []; showDidCopy = false; startTime = ""; truncatedDesc = ""; @@ -847,12 +902,21 @@ export default class ProjectViewView extends Vue { ); } - openGiftDialog(contact?: libsUtil.GiverReceiverInputInfo) { - (this.$refs.customGiveDialog as GiftedDialog).open( + openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) { + (this.$refs.giveDialogToThis as GiftedDialog).open( contact, undefined, undefined, - "Given by " + (contact?.name || "someone not named"), + (contact?.name || "Someone not named") + ` gave to this project`, + ); + } + + openGiftDialogFromProject() { + (this.$refs.giveDialogFromThis as GiftedDialog).open( + undefined, + { did: this.activeDid, name: "You" }, + undefined, + `This project gave to you`, ); } @@ -896,7 +960,7 @@ export default class ProjectViewView extends Vue { const giver: libsUtil.GiverReceiverInputInfo = { did: libsUtil.offerGiverDid(offerRecord), }; - (this.$refs.customGiveDialog as GiftedDialog).open( + (this.$refs.giveDialogToThis as GiftedDialog).open( giver, undefined, offer.handleId, @@ -932,20 +996,70 @@ export default class ProjectViewView extends Vue { } } - checkIsConfirmable(give: GiveSummaryRecord) { + /** + * @param confirmerIdList optional list of DIDs who confirmed; if missing, doesn't do a full server check + */ + checkIsConfirmable(give: GiveSummaryRecord, confirmerIdList?: string[]) { const giveDetails: GenericCredWrapper<GiveVerifiableCredential> = { ...BLANK_GENERIC_SERVER_RECORD, claim: give.fullClaim, claimType: "GiveAction", - issuer: give.agentDid, + issuer: give.issuerDid, }; return libsUtil.isGiveRecordTheUserCanConfirm( this.isRegistered, giveDetails, this.activeDid, + confirmerIdList, + ); + } + + shallowNotifyWhyCannotConfirm(give: GiveSummaryRecord) { + const confirmerIds = this.recentlyCheckedAndUnconfirmableJwts.includes( + give.jwtId, + ) + ? [this.activeDid] + : []; + libsUtil.notifyWhyCannotConfirm( + this.$notify, + this.isRegistered, + "GiveAction", + give, + this.activeDid, + confirmerIds, ); } + async deepCheckConfirmable(give: GiveSummaryRecord) { + this.checkingConfirmationForJwtId = give.jwtId; + const confirmerInfo: libsUtil.ConfirmerData | undefined = + await libsUtil.retrieveConfirmerIdList( + this.apiServer, + give.jwtId, + give.issuerDid, + this.activeDid, + ); + if ( + this.checkIsConfirmable(give, confirmerInfo?.confirmerIdList as string[]) + ) { + this.confirmConfirmClaim(give); + } else { + this.recentlyCheckedAndUnconfirmableJwts = [ + ...this.recentlyCheckedAndUnconfirmableJwts, + give.jwtId, + ]; + libsUtil.notifyWhyCannotConfirm( + this.$notify, + this.isRegistered, + "GiveAction", + give, + this.activeDid, + confirmerInfo?.confirmerIdList as string[], + ); + } + this.checkingConfirmationForJwtId = ""; + } + confirmConfirmClaim(give: GiveSummaryRecord) { this.$notify( { @@ -994,6 +1108,10 @@ export default class ProjectViewView extends Vue { }, 5000, ); + this.recentlyCheckedAndUnconfirmableJwts = [ + ...this.recentlyCheckedAndUnconfirmableJwts, + give.jwtId, + ]; } else { console.error("Got error submitting the confirmation:", result); const message =