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