<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> <div class="mb-8"> <h1 class="text-2xl">Quick Action</h1> <p>Choose a contact to whom to show appreciation:</p> <!-- similar contact selection code is in multiple places --> <div class="px-4"> <button v-for="contact in allContacts" :key="contact.did" @click="openDialog(contact)" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" > {{ contact.name || "(no name)" }} </button> <span v-if="allContacts.length > 0"> or </span> <button @click="openDialog()" class="text-blue-500"> someone not specified </button> </div> </div> <GiftedDialog ref="customDialog" @dialog-result="handleDialogResult" message="Received from" > </GiftedDialog> <div> <h1 class="text-2xl">Latest Activity</h1> <span :class="{ hidden: isHiddenSpinner }"> <fa icon="spinner" class="fa-spin-pulse"></fa> Loading… </span> <ul> <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 py-2 mb-2 font-bold uppercase text-sm" v-if="record.jwtId == feedLastViewedId" > You've seen all claims below: </div> <div class="flex"> <fa icon="gift" class="fa-fw flex-none 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> <AlertMessage :alertTitle="alertTitle" :alertMessage="alertMessage" ></AlertMessage> </section> </template> <script lang="ts"> import { Component, Vue } from "vue-facing-decorator"; import GiftedDialog from "@/components/GiftedDialog.vue"; import { db, accountsDB } from "@/db"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { accessToken } from "@/libs/crypto"; import { createAndSubmitGive, didInfo } from "@/libs/endorserServer"; import { Account } from "@/db/tables/accounts"; import { Contact } from "@/db/tables/contacts"; import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav"; @Component({ components: { GiftedDialog, AlertMessage, QuickNav }, }) export default class HomeView extends Vue { activeDid = ""; allAccounts: Array<Account> = []; allContacts: Array<Contact> = []; apiServer = ""; feedAllLoaded = false; feedData = []; feedPreviousOldestId = null; feedLastViewedId = null; isHiddenSpinner = true; alertTitle = ""; alertMessage = ""; public async getIdentity(activeDid) { await accountsDB.open(); const accounts = await accountsDB.accounts.toArray(); const account = R.find((acc) => acc.did === activeDid, accounts); 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) { const token = await accessToken(identity); const headers = { "Content-Type": "application/json", Authorization: "Bearer " + token, }; return headers; } async created() { try { await accountsDB.open(); this.allAccounts = await accountsDB.accounts.toArray(); await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.apiServer = settings?.apiServer || ""; this.activeDid = settings?.activeDid || ""; this.allContacts = await db.contacts.toArray(); this.feedLastViewedId = settings?.lastViewedClaimId; this.updateAllFeed(); } catch (err) { this.alertTitle = "Error"; this.alertMessage = err.userMessage || "There was an error retrieving the latest sweet, sweet action."; } } public async buildHeaders() { const headers = { "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 || "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, null, 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.alertMessage = e.userMessage || "There was an error retrieving feed data."; this.alertTitle = "Error"; }); this.isHiddenSpinner = true; } public async retrieveClaims(endorserApiServer, identifier, beforeId) { 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) { let claim = giveRecord.fullClaim; if (claim.claim) { claim = claim.claim; } // agent.did is for legacy data, before March 2023 const giverDid = claim.agent?.identifier || claim.agent?.did || giveRecord.issuer; const giverInfo = didInfo( giverDid, this.activeDid, this.allAccounts, this.allContacts, ); const gaveAmount = claim.object?.amountOfThisGood ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) : claim.description || "something unknown"; // recipient.did is for legacy data, before March 2023 const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did; const gaveRecipientInfo = gaveRecipientId ? " to " + didInfo( gaveRecipientId, this.activeDid, this.allAccounts, this.allContacts, ) : ""; return giverInfo + " gave " + gaveAmount + gaveRecipientInfo; } displayAmount(code, amt) { return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1); } currencyShortWordForCode(unitCode, single) { return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; } openDialog(giver) { this.$refs.customDialog.open(giver); } handleDialogResult(result) { if (result.action === "confirm") { return new Promise((resolve) => { this.recordGive(result.contact?.did, result.description, result.hours); resolve(); }); } else { // action was "cancel" so do nothing } } /** * * @param giverDid may be null * @param description may be an empty string * @param hours may be 0 */ public async recordGive(giverDid, description, hours) { if (!this.activeDid) { this.setAlert( "Error", "You must select an identity before you can record a give.", ); return; } if (!description && !hours) { this.setAlert( "Error", "You must enter a description or some number of hours.", ); return; } try { const identity = await this.getIdentity(this.activeDid); const result = await createAndSubmitGive( this.axios, this.apiServer, identity, giverDid, this.activeDid, description, hours, ); if (isGiveCreationError(result)) { const errorMessage = getGiveCreationErrorMessage(result); console.log("Error with give result:", result); this.setAlert( "Error", errorMessage || "There was an error recording the give.", ); } else { this.setAlert("Success", "That gift was recorded."); } } catch (error) { console.log("Error with give caught:", error); this.setAlert( "Error", getGiveErrorMessage(error) || "There was an error recording the give.", ); } } private setAlert(title, message) { this.alertTitle = title; this.alertMessage = message; } // Helper functions for readability isGiveCreationError(result) { return result.status !== 201 || result.data?.error; } getGiveCreationErrorMessage(result) { return result.data?.error?.message; } getGiveErrorMessage(error) { return error.userMessage || error.response?.data?.error?.message; } } </script>