Trent Larson
7 months ago
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