5 changed files with 355 additions and 2 deletions
			
			
		| @ -0,0 +1,337 @@ | |||
| <template> | |||
|   <QuickNav selected="Contacts" /> | |||
|   <TopMessage /> | |||
| 
 | |||
|   <!-- CONTENT --> | |||
|   <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> | |||
|     <!-- Breadcrumb --> | |||
|     <div id="ViewBreadcrumb" class="mb-8"> | |||
|       <h1 class="text-lg text-center font-light relative px-7"> | |||
|         <!-- Back --> | |||
|         <button | |||
|           @click="$router.go(-1)" | |||
|           class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" | |||
|         > | |||
|           <fa icon="chevron-left" class="fa-fw"></fa> | |||
|         </button> | |||
|         Identifier Details | |||
|       </h1> | |||
|     </div> | |||
| 
 | |||
|     <!-- Identity Details --> | |||
|     <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> | |||
|       <div> | |||
|         <h2 class="text-xl font-semibold"> | |||
|           {{ | |||
|             didInfoForContact(viewingDid, activeDid, contact, allMyDids) | |||
|               .displayName | |||
|           }} | |||
|         </h2> | |||
|         <span class="mt-2 text-xl font-semibold break-words"> | |||
|           {{ viewingDid }} | |||
|         </span> | |||
|       </div> | |||
|       <div class="flex justify-center mt-4"> | |||
|         <span v-if="contact?.profileImageUrl" class="flex justify-between"> | |||
|           <EntityIcon | |||
|             :icon-size="96" | |||
|             :profileImageUrl="contact?.profileImageUrl" | |||
|             class="inline-block align-text-bottom border border-slate-300 rounded" | |||
|             @click="showLargeIdenticonUrl = contact?.profileImageUrl" | |||
|           /> | |||
|         </span> | |||
|       </div> | |||
|       <div class="mt-4"> | |||
|         <div class="flex justify-center">Auto-Generated Icon:</div> | |||
|         <div class="flex justify-center"> | |||
|           <EntityIcon | |||
|             :entityId="viewingDid" | |||
|             :iconSize="64" | |||
|             class="inline-block align-middle border border-slate-300 rounded-md mr-1" | |||
|             @click="showLargeIdenticonId = viewingDid" | |||
|           /> | |||
|         </div> | |||
|       </div> | |||
|       <div | |||
|         v-if="showLargeIdenticonId || showLargeIdenticonUrl" | |||
|         class="fixed z-[100] top-0 inset-x-0 w-full" | |||
|       > | |||
|         <div | |||
|           class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50" | |||
|         > | |||
|           <EntityIcon | |||
|             :entityId="showLargeIdenticonId" | |||
|             :iconSize="512" | |||
|             :profileImageUrl="showLargeIdenticonUrl" | |||
|             class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg" | |||
|             @click=" | |||
|               showLargeIdenticonId = undefined; | |||
|               showLargeIdenticonUrl = undefined; | |||
|             " | |||
|           /> | |||
|         </div> | |||
|       </div> | |||
|     </div> | |||
| 
 | |||
|     <!-- Loading Animation --> | |||
|     <div | |||
|       class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full" | |||
|       v-if="isLoading" | |||
|     > | |||
|       <fa icon="spinner" class="fa-spin-pulse"></fa> | |||
|     </div> | |||
|     <!-- Results List --> | |||
|     <div v-if="claims.length > 0" class="mt-4"> | |||
|       <div class="text-l font-bold text-center">Claims That Involve Them</div> | |||
|     </div> | |||
|     <InfiniteScroll @reached-bottom="loadMoreData"> | |||
|       <ul> | |||
|         <li | |||
|           class="border-b border-slate-300" | |||
|           v-for="claim in claims" | |||
|           :key="claim.handleId" | |||
|         > | |||
|           <div class="grid grid-cols-12 gap-4"> | |||
|             <span class="col-span-2"> | |||
|               {{ claim.issuedAt.substring(0, 10) }} | |||
|             </span> | |||
|             <span class="col-span-2"> | |||
|               {{ capitalizeAndInsertSpacesBeforeCaps(claim.claimType) }} | |||
|             </span> | |||
|             <span class="col-span-2"> | |||
|               {{ claimAmount(claim) }} | |||
|             </span> | |||
|             <span class="col-span-5"> | |||
|               {{ claimDescription(claim) }} | |||
|             </span> | |||
|             <span class="col-span-1"> | |||
|               <a | |||
|                 @click="onClickLoadClaim(claim.handleId)" | |||
|                 class="cursor-pointer" | |||
|               > | |||
|                 <fa icon="file-lines" class="pl-2 pt-1 text-blue-500" /> | |||
|               </a> | |||
|             </span> | |||
|           </div> | |||
|         </li> | |||
|       </ul> | |||
|     </InfiniteScroll> | |||
| 
 | |||
|     <div | |||
|       v-if="!isLoading && claims.length === 0" | |||
|       class="flex justify-center mt-4" | |||
|     > | |||
|       <span>No Claims Visible to You Involve Them</span> | |||
|     </div> | |||
|   </section> | |||
| </template> | |||
| 
 | |||
| <script lang="ts"> | |||
| import { Component, Vue } from "vue-facing-decorator"; | |||
| 
 | |||
| import QuickNav from "@/components/QuickNav.vue"; | |||
| import InfiniteScroll from "@/components/InfiniteScroll.vue"; | |||
| import TopMessage from "@/components/TopMessage.vue"; | |||
| import { NotificationIface } from "@/constants/app"; | |||
| import { accountsDB, db } from "@/db/index"; | |||
| import { Contact } from "@/db/tables/contacts"; | |||
| import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; | |||
| import { accessToken } from "@/libs/crypto"; | |||
| import { | |||
|   capitalizeAndInsertSpacesBeforeCaps, | |||
|   didInfoForContact, | |||
|   displayAmount, | |||
|   GenericCredWrapper, | |||
|   GenericVerifiableCredential, | |||
|   GiveVerifiableCredential, | |||
|   OfferVerifiableCredential, | |||
| } from "@/libs/endorserServer"; | |||
| import EntityIcon from "@/components/EntityIcon.vue"; | |||
| 
 | |||
| @Component({ | |||
|   components: { | |||
|     EntityIcon, | |||
|     InfiniteScroll, | |||
|     QuickNav, | |||
|     TopMessage, | |||
|   }, | |||
| }) | |||
| export default class DIDView extends Vue { | |||
|   $notify!: (notification: NotificationIface, timeout?: number) => void; | |||
| 
 | |||
|   activeDid = ""; | |||
|   allMyDids: Array<string> = []; | |||
|   apiServer = ""; | |||
|   claims: Array<GenericCredWrapper> = []; | |||
|   contact?: Contact; | |||
|   hitEnd = false; | |||
|   isLoading = false; | |||
|   searchBox: { name: string; bbox: BoundingBox } | null = null; | |||
|   showLargeIdenticonId?: string; | |||
|   showLargeIdenticonUrl?: string; | |||
|   viewingDid?: string; | |||
| 
 | |||
|   capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps; | |||
|   didInfoForContact = didInfoForContact; | |||
|   displayAmount = displayAmount; | |||
| 
 | |||
|   async mounted() { | |||
|     await db.open(); | |||
|     const settings = await db.settings.get(MASTER_SETTINGS_KEY); | |||
|     this.activeDid = (settings?.activeDid as string) || ""; | |||
|     this.apiServer = (settings?.apiServer as string) || ""; | |||
| 
 | |||
|     const pathParam = window.location.pathname.substring("/did/".length); | |||
|     if (pathParam) { | |||
|       this.viewingDid = decodeURIComponent(pathParam); | |||
|       this.contact = await db.contacts.get(this.viewingDid); | |||
|       await this.loadClaimsAbout(); | |||
|     } else { | |||
|       this.$notify( | |||
|         { | |||
|           group: "alert", | |||
|           type: "danger", | |||
|           title: "Error", | |||
|           text: "No claim ID was provided.", | |||
|         }, | |||
|         -1, | |||
|       ); | |||
|     } | |||
| 
 | |||
|     await accountsDB.open(); | |||
|     const allAccounts = await accountsDB.accounts.toArray(); | |||
|     this.allMyDids = allAccounts.map((acc) => acc.did); | |||
|   } | |||
| 
 | |||
|   public async buildHeaders(): Promise<HeadersInit> { | |||
|     const headers: HeadersInit = { | |||
|       "Content-Type": "application/json", | |||
|     }; | |||
| 
 | |||
|     if (this.activeDid) { | |||
|       await accountsDB.open(); | |||
|       const allAccounts = await accountsDB.accounts.toArray(); | |||
|       const account = allAccounts.find((acc) => acc.did === this.activeDid); | |||
|       const identity = JSON.parse((account?.identity as string) || "null"); | |||
| 
 | |||
|       if (!identity) { | |||
|         throw new Error( | |||
|           "An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.", | |||
|         ); | |||
|       } | |||
| 
 | |||
|       headers["Authorization"] = "Bearer " + (await accessToken(identity)); | |||
|     } else { | |||
|       // it's OK without auth... we just won't get any identifiers | |||
|     } | |||
|     return headers; | |||
|   } | |||
| 
 | |||
|   /** | |||
|    * Data loader used by infinite scroller | |||
|    * @param payload is the flag from the InfiniteScroll indicating if it should load | |||
|    **/ | |||
|   async loadMoreData(payload: boolean) { | |||
|     if (this.claims.length > 0 && !this.hitEnd && payload) { | |||
|       this.loadClaimsAbout(); | |||
|     } | |||
|   } | |||
| 
 | |||
|   public async loadClaimsAbout() { | |||
|     if (!this.viewingDid) { | |||
|       console.error("This should never be called without a DID."); | |||
|       return; | |||
|     } | |||
| 
 | |||
|     const queryParams = "claimContents=" + encodeURIComponent(this.viewingDid); | |||
|     let postfix = ""; | |||
|     if (this.claims.length > 0) { | |||
|       postfix = "&beforeId=" + this.claims[this.claims.length - 1].id; | |||
|     } | |||
| 
 | |||
|     try { | |||
|       this.isLoading = true; | |||
|       const response = await fetch( | |||
|         this.apiServer + "/api/v2/report/claims?" + queryParams + postfix, | |||
|         { | |||
|           method: "GET", | |||
|           headers: await this.buildHeaders(), | |||
|         }, | |||
|       ); | |||
| 
 | |||
|       if (response.status !== 200) { | |||
|         const details = await response.text(); | |||
|         console.error("Problem with full search:", details); | |||
|         this.$notify( | |||
|           { | |||
|             group: "alert", | |||
|             type: "danger", | |||
|             title: "Error", | |||
|             text: `There was a problem accessing the server. Try again later.`, | |||
|           }, | |||
|           5000, | |||
|         ); | |||
|         return; | |||
|       } | |||
| 
 | |||
|       const results = await response.json(); | |||
|       this.claims = this.claims.concat(results.data); | |||
|       this.hitEnd = !results.hitLimit; | |||
| 
 | |||
|       // eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
|     } catch (e: any) { | |||
|       console.error("Error with feed load:", e); | |||
|       this.$notify( | |||
|         { | |||
|           group: "alert", | |||
|           type: "danger", | |||
|           title: "Error", | |||
|           text: e.userMessage || "There was a problem retrieving claims.", | |||
|         }, | |||
|         -1, | |||
|       ); | |||
|     } finally { | |||
|       this.isLoading = false; | |||
|     } | |||
|   } | |||
| 
 | |||
|   onClickLoadClaim(jwtId: string) { | |||
|     const route = { | |||
|       path: "/claim/" + encodeURIComponent(jwtId), | |||
|     }; | |||
|     this.$router.push(route); | |||
|   } | |||
| 
 | |||
|   public claimAmount(claim: GenericVerifiableCredential) { | |||
|     if (claim.claimType === "GiveAction") { | |||
|       const giveClaim = claim.claim as GiveVerifiableCredential; | |||
|       if (giveClaim.object?.unitCode && giveClaim.object?.amountOfThisGood) { | |||
|         return displayAmount( | |||
|           giveClaim.object.unitCode, | |||
|           giveClaim.object.amountOfThisGood, | |||
|         ); | |||
|       } else { | |||
|         return ""; | |||
|       } | |||
|     } else if (claim.claimType === "Offer") { | |||
|       const offerClaim = claim.claim as OfferVerifiableCredential; | |||
|       if ( | |||
|         offerClaim.includesObject?.unitCode && | |||
|         offerClaim.includesObject?.amountOfThisGood | |||
|       ) { | |||
|         return displayAmount( | |||
|           offerClaim.includesObject.unitCode, | |||
|           offerClaim.includesObject.amountOfThisGood, | |||
|         ); | |||
|       } else { | |||
|         return ""; | |||
|       } | |||
|     } | |||
|     return ""; | |||
|   } | |||
| 
 | |||
|   claimDescription(claim: GenericVerifiableCredential) { | |||
|     return claim.claim.name || claim.claim.description || ""; | |||
|   } | |||
| } | |||
| </script> | |||
					Loading…
					
					
				
		Reference in new issue