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&hellip;
-      </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 =