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