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