<template> <QuickNav selected="Home"></QuickNav> <!-- CONTENT --> <section id="Content" class="p-6 pb-24"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> Time Safari </h1> <!-- show the actions for recognizing a give --> <div class="mb-8"> <div v-if="!activeDid"> To record others' giving, <router-link :to="{ name: 'start' }" class="text-blue-500"> create your identifier.</router-link > </div> <div v-else-if="!isRegistered"> To record others' giving, someone must register your account, so show them <router-link :to="{ name: 'contact-qr' }" class="text-blue-500"> your identity info</router-link > and then <router-link :to="{ name: 'account' }" class="text-blue-500"> check your limits.</router-link > </div> <div v-else> <!-- activeDid && isRegistered --> <h2 class="text-xl font-bold">Record Something Given</h2> <ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> <li @click="openDialog()"> <EntityIcon :entityId="null" :iconSize="64" class="mx-auto border border-slate-300 rounded-md mb-1" ></EntityIcon> <h3 class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden" > Anonymous/Unnamed </h3> </li> <li v-for="contact in allContacts" :key="contact.did" @click="openDialog(contact)" > <EntityIcon :entityId="contact.did" :iconSize="64" class="mx-auto border border-slate-300 rounded-md mb-1" ></EntityIcon> <h3 class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden" > {{ contact.name || contact.did }} </h3> </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) --> <router-link v-if="allContacts.length >= 7" :to="{ name: 'contact-gives' }" class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md" > Show More Contacts… </router-link> <!-- If there are no contacts, show this instead: --> <div class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500" v-if="allContacts.length === 0" > (No contacts to show.) </div> </div> </div> <GiftedDialog ref="customDialog" message="Received from" showGivenToUser="true" /> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <h2 class="text-xl font-bold mb-4">Latest Activity</h2> <div :class="{ hidden: isHiddenSpinner }"> <p class="text-slate-500 text-center italic mt-4 mb-4"> <fa icon="spinner" class="fa-spin-pulse"></fa> Loading… </p> </div> <ul class="border-t border-slate-300"> <li class="border-b border-slate-300 py-2" v-for="record in feedData" :key="record.jwtId" > <div class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm" v-if="record.jwtId == feedLastViewedId" > You've seen all the following before </div> <div class="flex"> <fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa> <!-- icon values: "coins" = money; "clock" = time; "gift" = others --> <span class="">{{ this.giveDescription(record) }}</span> </div> </li> </ul> </div> </section> </template> <script lang="ts"> import { Component, Vue } from "vue-facing-decorator"; import GiftedDialog from "@/components/GiftedDialog.vue"; import { db, accountsDB } from "@/db/index"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { accessToken } from "@/libs/crypto"; import { didInfo, GiverInputInfo, GiveServerRecord, } from "@/libs/endorserServer"; import { Contact } from "@/db/tables/contacts"; import QuickNav from "@/components/QuickNav.vue"; import EntityIcon from "@/components/EntityIcon.vue"; import { IIdentifier } from "@veramo/core"; import { Account } from "@/db/tables/accounts"; interface Notification { group: string; type: string; title: string; text: string; } @Component({ components: { GiftedDialog, QuickNav, EntityIcon }, }) export default class HomeView extends Vue { $notify!: (notification: Notification, timeout?: number) => void; activeDid = ""; allContacts: Array<Contact> = []; allMyDids: Array<string> = []; apiServer = ""; feedAllLoaded = false; feedData = []; feedPreviousOldestId?: string; feedLastViewedId?: string; isHiddenSpinner = true; isRegistered = false; numAccounts = 0; async beforeCreate() { await accountsDB.open(); this.numAccounts = await accountsDB.accounts.count(); } public async getIdentity(activeDid: string) { await accountsDB.open(); const account = (await accountsDB.accounts .where("did") .equals(activeDid) .first()) as Account; const identity = JSON.parse(account?.identity || "null"); if (!identity) { throw new Error( "Attempted to load Give records with no identity available.", ); } return identity; } public async getHeaders(identity: IIdentifier) { const token = await accessToken(identity); const headers = { "Content-Type": "application/json", Authorization: "Bearer " + token, }; return headers; } async created() { try { await accountsDB.open(); const allAccounts = await accountsDB.accounts.toArray(); this.allMyDids = allAccounts.map((acc) => acc.did); await db.open(); const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; this.apiServer = settings?.apiServer || ""; this.activeDid = settings?.activeDid || ""; this.allContacts = await db.contacts.toArray(); this.feedLastViewedId = settings?.lastViewedClaimId; this.isRegistered = !!settings?.isRegistered; this.updateAllFeed(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { this.$notify( { group: "alert", type: "danger", title: "Error", text: err.userMessage || "There was an error retrieving the latest sweet, sweet action.", }, -1, ); } } public async buildHeaders() { 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, ) as Account; const identity = JSON.parse(account?.identity || "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.", ); } headers["Authorization"] = "Bearer " + (await accessToken(identity)); } else { // it's OK without auth... we just won't get any identifiers } return headers; } public async updateAllFeed() { this.isHiddenSpinner = false; await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId) .then(async (results) => { if (results.data.length > 0) { this.feedData = this.feedData.concat(results.data); this.feedAllLoaded = results.hitLimit; this.feedPreviousOldestId = results.data[results.data.length - 1].jwtId; if ( this.feedLastViewedId == null || this.feedLastViewedId < results.data[0].jwtId ) { await db.open(); db.settings.update(MASTER_SETTINGS_KEY, { lastViewedClaimId: results.data[0].jwtId, }); // but not for this page because we need to remember what it was before } } }) .catch((e) => { console.log("Error with feed load:", e); this.$notify( { group: "alert", type: "danger", title: "Export Error", text: e.userMessage || "There was an error retrieving feed data.", }, -1, ); }); this.isHiddenSpinner = true; } public async retrieveClaims(endorserApiServer: string, beforeId?: string) { const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const response = await fetch( endorserApiServer + "/api/v2/report/gives?" + beforeQuery, { method: "GET", headers: await this.buildHeaders(), }, ); if (response.status !== 200) { throw await response.text(); } const results = await response.json(); if (results.data) { return results; } else { throw JSON.stringify(results); } } giveDescription(giveRecord: GiveServerRecord) { // similar code is in endorser-mobile utility.ts // claim.claim happen for some claims wrapped in a Verifiable Credential // eslint-disable-next-line @typescript-eslint/no-explicit-any const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim; // agent.did is for legacy data, before March 2023 // eslint-disable-next-line @typescript-eslint/no-explicit-any const giverDid = claim.agent?.identifier || (claim.agent as any)?.did; const giverInfo = didInfo( giverDid, this.activeDid, this.allMyDids, this.allContacts, ); let gaveAmount = claim.object?.amountOfThisGood ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) : ""; if (claim.description) { if (gaveAmount) { gaveAmount = gaveAmount + ", and also: "; } gaveAmount = gaveAmount + claim.description; } if (!gaveAmount) { gaveAmount = "something not described"; } // recipient.did is for legacy data, before March 2023 const gaveRecipientId = // eslint-disable-next-line @typescript-eslint/no-explicit-any claim.recipient?.identifier || (claim.recipient as any)?.did; const gaveRecipientInfo = gaveRecipientId ? " to " + didInfo( gaveRecipientId, this.activeDid, this.allMyDids, this.allContacts, ) : ""; return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount; } displayAmount(code: string, amt: number) { return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1); } currencyShortWordForCode(unitCode: string, single: boolean) { return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; } openDialog(giver: GiverInputInfo) { (this.$refs.customDialog as GiftedDialog).open(giver); } } </script>