forked from jsnbuchanan/crowd-funder-for-time-pwa
add page to view all claims about a DID (which we'll have to restrict to visible people soon)
This commit is contained in:
@@ -161,7 +161,9 @@
|
|||||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
>
|
>
|
||||||
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
<span class="font-semibold text-lg">{{ notification.title }}</span>
|
<span class="font-semibold text-lg">
|
||||||
|
{{ notification.title }}
|
||||||
|
</span>
|
||||||
<p class="text-sm mb-2">{{ notification.text }}</p>
|
<p class="text-sm mb-2">{{ notification.text }}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export interface GenericVerifiableCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericCredWrapper extends GenericVerifiableCredential {
|
export interface GenericCredWrapper extends GenericVerifiableCredential {
|
||||||
handleId?: string;
|
handleId: string;
|
||||||
id: string;
|
id: string;
|
||||||
issuedAt: string;
|
issuedAt: string;
|
||||||
issuer: string;
|
issuer: string;
|
||||||
@@ -64,6 +64,7 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper = {
|
|||||||
"@context": SCHEMA_ORG_CONTEXT,
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
"@type": "",
|
"@type": "",
|
||||||
claim: {},
|
claim: {},
|
||||||
|
handleId: "",
|
||||||
id: "",
|
id: "",
|
||||||
issuedAt: "",
|
issuedAt: "",
|
||||||
issuer: "",
|
issuer: "",
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "contacts",
|
name: "contacts",
|
||||||
component: () => import("../views/ContactsView.vue"),
|
component: () => import("../views/ContactsView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/did/:did?",
|
||||||
|
name: "did",
|
||||||
|
component: () => import("../views/DIDView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/discover",
|
path: "/discover",
|
||||||
name: "discover",
|
name: "discover",
|
||||||
|
|||||||
@@ -118,6 +118,14 @@
|
|||||||
>
|
>
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
path: '/did/' + encodeURIComponent(contact.did),
|
||||||
|
}"
|
||||||
|
title="See more about this DID"
|
||||||
|
>
|
||||||
|
<fa icon="circle-info" class="fa-fw ml-2 text-blue-500 rounded" />
|
||||||
|
</router-link>
|
||||||
<span v-show="showDidCopy">Copied DID</span>
|
<span v-show="showDidCopy">Copied DID</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
||||||
|
|||||||
337
src/views/DIDView.vue
Normal file
337
src/views/DIDView.vue
Normal file
@@ -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>
|
||||||
Reference in New Issue
Block a user