You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							347 lines
						
					
					
						
							9.9 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							347 lines
						
					
					
						
							9.9 KiB
						
					
					
				| <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>
 | |
| 
 |