884 lines
26 KiB
884 lines
26 KiB
<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 id="ViewHeading" class="text-lg text-center font-light relative px-7">
|
|
<!-- Back -->
|
|
<button
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
@click="$router.go(-1)"
|
|
>
|
|
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
|
</button>
|
|
Identifier Details
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Identity Details -->
|
|
<div
|
|
v-if="!!contactFromDid"
|
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"
|
|
>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">
|
|
{{ contactFromDid?.name || "(no name)" }}
|
|
<router-link
|
|
:to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }"
|
|
>
|
|
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
|
</router-link>
|
|
</h2>
|
|
<button
|
|
class="ml-2 mr-2 mt-4"
|
|
@click="showDidDetails = !showDidDetails"
|
|
>
|
|
Details
|
|
<font-awesome
|
|
v-if="showDidDetails"
|
|
icon="chevron-down"
|
|
class="text-blue-400"
|
|
/>
|
|
<font-awesome v-else icon="chevron-right" class="text-blue-400" />
|
|
</button>
|
|
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
|
<pre
|
|
v-if="showDidDetails"
|
|
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
|
|
>{{ contactYaml }}</pre
|
|
>
|
|
</div>
|
|
<div class="flex justify-center mt-4">
|
|
<span
|
|
v-if="contactFromDid?.profileImageUrl"
|
|
class="flex justify-between"
|
|
>
|
|
<EntityIcon
|
|
:icon-size="96"
|
|
:profile-image-url="contactFromDid?.profileImageUrl"
|
|
class="inline-block align-text-bottom border border-slate-300 rounded"
|
|
@click="showLargeIdenticonUrl = contactFromDid?.profileImageUrl"
|
|
/>
|
|
</span>
|
|
</div>
|
|
<div class="flex justify-between mt-4">
|
|
<div class="flex items-center">
|
|
<div v-if="activeDid" class="flex justify-between">
|
|
<div>
|
|
<button
|
|
v-if="
|
|
contactFromDid?.seesMe && contactFromDid.did !== activeDid
|
|
"
|
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
|
title="They can see you"
|
|
@click="confirmSetVisibility(contactFromDid, false)"
|
|
>
|
|
<font-awesome icon="eye" class="fa-fw" />
|
|
</button>
|
|
<button
|
|
v-else-if="
|
|
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
|
|
"
|
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
|
title="They cannot see you"
|
|
@click="confirmSetVisibility(contactFromDid, true)"
|
|
>
|
|
<font-awesome icon="eye-slash" class="fa-fw" />
|
|
</button>
|
|
|
|
<button
|
|
v-if="contactFromDid?.did !== activeDid"
|
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
|
title="Check Visibility"
|
|
@click="checkVisibility(contactFromDid)"
|
|
>
|
|
<font-awesome icon="rotate" class="fa-fw" />
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
v-if="contactFromDid?.did !== activeDid"
|
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
|
title="Registration"
|
|
@click="confirmRegister(contactFromDid)"
|
|
>
|
|
<font-awesome
|
|
v-if="contactFromDid?.registered"
|
|
icon="person-circle-check"
|
|
class="fa-fw"
|
|
/>
|
|
<font-awesome
|
|
v-else
|
|
icon="person-circle-question"
|
|
class="fa-fw"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
|
title="Delete"
|
|
@click="confirmDeleteContact(contactFromDid)"
|
|
>
|
|
<font-awesome icon="trash-can" class="fa-fw" />
|
|
</button>
|
|
</div>
|
|
<div v-if="!contactFromDid?.profileImageUrl">
|
|
<div>Auto-Generated Icon</div>
|
|
<div class="flex justify-center">
|
|
<EntityIcon
|
|
:entity-id="viewingDid"
|
|
:icon-size="64"
|
|
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
|
@click="showLargeIdenticonId = viewingDid"
|
|
/>
|
|
</div>
|
|
</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
|
|
:entity-id="showLargeIdenticonId"
|
|
:icon-size="512"
|
|
:profile-image-url="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>
|
|
<div v-else class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
|
<!-- !contactFromDid -->
|
|
<div>
|
|
<h2 class="text-xl font-semibold">
|
|
{{ isMyDid ? "You" : "(no name)" }}
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Animation -->
|
|
<div
|
|
v-if="isLoading"
|
|
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
|
>
|
|
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
|
|
</div>
|
|
<!-- Results List -->
|
|
<div v-if="claims.length > 0" class="mt-4">
|
|
<div class="text-l font-bold text-center">
|
|
Claims That Involve {{ isMyDid ? "You" : "Them" }}
|
|
</div>
|
|
</div>
|
|
<InfiniteScroll @reached-bottom="loadMoreData">
|
|
<ul>
|
|
<li
|
|
v-for="claim in claims"
|
|
:key="claim.handleId"
|
|
class="border-b border-slate-300"
|
|
>
|
|
<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 class="cursor-pointer" @click="onClickLoadClaim(claim.id)">
|
|
<font-awesome
|
|
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 v-if="isMyDid">You have no claims yet.</span>
|
|
<span v-else>They are in no claims visible to you.</span>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { AxiosError } from "axios";
|
|
import * as yaml from "js-yaml";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
|
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import InfiniteScroll from "../components/InfiniteScroll.vue";
|
|
import TopMessage from "../components/TopMessage.vue";
|
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import { BoundingBox } from "../db/tables/settings";
|
|
import * as databaseUtil from "../db/databaseUtil";
|
|
import {
|
|
GenericCredWrapper,
|
|
GenericVerifiableCredential,
|
|
GiveActionClaim,
|
|
OfferClaim,
|
|
} from "../interfaces";
|
|
import {
|
|
capitalizeAndInsertSpacesBeforeCaps,
|
|
didInfoForContact,
|
|
displayAmount,
|
|
getHeaders,
|
|
register,
|
|
setVisibilityUtil,
|
|
} from "../libs/endorserServer";
|
|
import * as libsUtil from "../libs/util";
|
|
import EntityIcon from "../components/EntityIcon.vue";
|
|
import { logger } from "../utils/logger";
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
|
|
/**
|
|
* DIDView Component
|
|
*
|
|
* Displays detailed information about a DID (Decentralized Identifier) entity, including:
|
|
* - Basic identity information (name, profile image)
|
|
* - Contact management controls (visibility, registration status)
|
|
* - Associated claims and their details
|
|
*
|
|
* The view supports both viewing one's own DID and other contacts' DIDs.
|
|
* It provides infinite scrolling for claims and interactive controls for contact management.
|
|
*/
|
|
@Component({
|
|
components: {
|
|
EntityIcon,
|
|
InfiniteScroll,
|
|
QuickNav,
|
|
TopMessage,
|
|
},
|
|
})
|
|
export default class DIDView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
$route!: RouteLocationNormalizedLoaded;
|
|
$router!: Router;
|
|
|
|
libsUtil = libsUtil;
|
|
yaml = yaml;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
|
contactFromDid?: Contact;
|
|
contactYaml = "";
|
|
hitEnd = false;
|
|
isLoading = false;
|
|
isMyDid = false;
|
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
|
showDidDetails = false;
|
|
showLargeIdenticonId?: string;
|
|
showLargeIdenticonUrl?: string;
|
|
viewingDid?: string;
|
|
|
|
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
|
|
didInfoForContact = didInfoForContact;
|
|
displayAmount = displayAmount;
|
|
|
|
/**
|
|
* Initializes the view with DID information
|
|
*
|
|
* Workflow:
|
|
* 1. Retrieves active account settings (DID and API server)
|
|
* 2. Determines which DID to display from URL params or defaults to active DID
|
|
* 3. Loads contact information if available
|
|
* 4. Loads associated claims
|
|
* 5. Determines if viewing own DID
|
|
*/
|
|
async mounted() {
|
|
await this.initializeSettings();
|
|
await this.determineDIDToDisplay();
|
|
if (this.viewingDid) {
|
|
await this.loadContactInformation();
|
|
await this.loadClaimsAbout();
|
|
await this.checkIfOwnDID();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes component settings from active account
|
|
*/
|
|
private async initializeSettings() {
|
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
|
if (USE_DEXIE_DB) {
|
|
settings = await retrieveSettingsForActiveAccount();
|
|
}
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
}
|
|
|
|
/**
|
|
* Determines which DID to display based on URL parameters
|
|
* Falls back to active DID if no parameter provided
|
|
*/
|
|
private async determineDIDToDisplay() {
|
|
const pathParam = window.location.pathname.substring("/did/".length);
|
|
let showDid = pathParam;
|
|
|
|
if (!showDid) {
|
|
showDid = this.activeDid;
|
|
if (showDid) {
|
|
this.notifyDefaultToActiveDID();
|
|
}
|
|
}
|
|
|
|
if (showDid) {
|
|
this.viewingDid = decodeURIComponent(showDid);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notifies user that we're showing their DID info by default
|
|
*/
|
|
private notifyDefaultToActiveDID() {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "toast",
|
|
title: "Your Info",
|
|
text: "No user was specified so showing your info.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Loads contact information for the viewing DID
|
|
* Updates contact YAML representation if contact exists
|
|
*/
|
|
private async loadContactInformation() {
|
|
if (!this.viewingDid) return;
|
|
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
const dbContacts = await platformService.dbQuery(
|
|
"SELECT * FROM contacts WHERE did = ?",
|
|
[this.viewingDid],
|
|
);
|
|
this.contactFromDid = databaseUtil.mapQueryResultToValues(
|
|
dbContacts,
|
|
)[0] as unknown as Contact;
|
|
if (USE_DEXIE_DB) {
|
|
this.contactFromDid = await db.contacts.get(this.viewingDid);
|
|
}
|
|
if (this.contactFromDid) {
|
|
this.contactYaml = yaml.dump(this.contactFromDid);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the viewing DID belongs to the current user
|
|
*/
|
|
private async checkIfOwnDID() {
|
|
if (!this.viewingDid) return;
|
|
|
|
const allAccountDids = await libsUtil.retrieveAccountDids();
|
|
this.isMyDid = allAccountDids.includes(this.viewingDid);
|
|
}
|
|
|
|
/**
|
|
* Loads additional claims when user scrolls to bottom
|
|
* Used by infinite scroll component to implement pagination
|
|
*
|
|
* @param payload - Boolean indicating if more data should be loaded
|
|
*/
|
|
async loadMoreData(payload: boolean) {
|
|
if (this.claims.length > 0 && !this.hitEnd && payload) {
|
|
this.loadClaimsAbout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prompts user to confirm contact deletion
|
|
* Shows additional warning if contact has visibility permissions
|
|
*
|
|
* @param contact - Contact object to be deleted
|
|
*/
|
|
confirmDeleteContact(contact: Contact) {
|
|
let message =
|
|
"Are you sure you want to remove " +
|
|
libsUtil.nameForContact(contact, false) +
|
|
" from your contact list?";
|
|
if (contact.seesMe) {
|
|
message +=
|
|
" Note that they can see your activity, so if you want to hide your activity from them then you should do that first.";
|
|
}
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Delete",
|
|
text: message,
|
|
onYes: async () => {
|
|
await this.deleteContact(contact);
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Deletes contact from local database and navigates back to contacts list
|
|
*
|
|
* @param contact - Contact object to be deleted
|
|
*/
|
|
async deleteContact(contact: Contact) {
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
await platformService.dbExec("DELETE FROM contacts WHERE did = ?", [
|
|
contact.did,
|
|
]);
|
|
if (USE_DEXIE_DB) {
|
|
await db.open();
|
|
await db.contacts.delete(contact.did);
|
|
}
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Deleted",
|
|
text: "Contact has been removed.",
|
|
},
|
|
3000,
|
|
);
|
|
this.$router.push({ name: "contacts" });
|
|
}
|
|
|
|
/**
|
|
* Prompts user to confirm registering a contact
|
|
* Shows additional warning if contact is already registered
|
|
*
|
|
* @param contact - Contact to be registered
|
|
*/
|
|
async confirmRegister(contact: Contact) {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Register",
|
|
text:
|
|
"Are you sure you want to register " +
|
|
libsUtil.nameForContact(this.contactFromDid, false) +
|
|
(contact.registered
|
|
? " -- especially since they are already marked as registered"
|
|
: "") +
|
|
"?",
|
|
onYes: async () => {
|
|
await this.register(contact);
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Registers a contact with the endorser server
|
|
* Updates local database with registration status
|
|
*
|
|
* @param contact - Contact to register
|
|
*/
|
|
async register(contact: Contact) {
|
|
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
|
|
|
|
try {
|
|
const regResult = await register(
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
contact,
|
|
);
|
|
if (regResult.success) {
|
|
contact.registered = true;
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
await platformService.dbExec(
|
|
"UPDATE contacts SET registered = ? WHERE did = ?",
|
|
[true, contact.did],
|
|
);
|
|
if (USE_DEXIE_DB) {
|
|
await db.contacts.update(contact.did, { registered: true });
|
|
}
|
|
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Registration Success",
|
|
text:
|
|
(contact.name || "That unnamed person") + " has been registered.",
|
|
},
|
|
5000,
|
|
);
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Registration Error",
|
|
text:
|
|
(regResult.error as string) ||
|
|
"Something went wrong during registration.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
logger.error("Error when registering:", error);
|
|
let userMessage = "There was an error.";
|
|
const serverError = error as AxiosError;
|
|
if (serverError) {
|
|
if (serverError.response?.data?.error?.message) {
|
|
userMessage = serverError.response.data.error.message;
|
|
} else if (serverError.message) {
|
|
userMessage = serverError.message; // Info for the user
|
|
} else {
|
|
userMessage = JSON.stringify(serverError.toJSON());
|
|
}
|
|
} else {
|
|
userMessage = error as string;
|
|
}
|
|
// Now set that error for the user to see.
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Registration Error",
|
|
text: userMessage,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads claims that involve the viewed DID
|
|
* Implements pagination using beforeId parameter
|
|
* Updates loading state and hit-end status
|
|
*/
|
|
public async loadClaimsAbout() {
|
|
if (!this.viewingDid) {
|
|
logger.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 getHeaders(this.activeDid),
|
|
},
|
|
);
|
|
|
|
if (response.status !== 200) {
|
|
const details = await response.text();
|
|
logger.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) {
|
|
logger.error("Error with feed load:", e);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: e.userMessage || "There was a problem retrieving claims.",
|
|
},
|
|
3000,
|
|
);
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Navigates to detailed claim view
|
|
*
|
|
* @param jwtId - JWT ID of the claim to view
|
|
*/
|
|
onClickLoadClaim(jwtId: string) {
|
|
const route = {
|
|
path: "/claim/" + encodeURIComponent(jwtId),
|
|
};
|
|
this.$router.push(route);
|
|
}
|
|
|
|
/**
|
|
* Extracts and formats claim amount information
|
|
* Handles different claim types (GiveAction, Offer)
|
|
*
|
|
* @param claim - Claim object to process
|
|
* @returns Formatted amount string or empty string if no amount
|
|
*/
|
|
public claimAmount(claim: GenericVerifiableCredential) {
|
|
if (claim.claimType === "GiveAction") {
|
|
const giveClaim = claim.claim as GiveActionClaim;
|
|
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 OfferClaim;
|
|
if (
|
|
offerClaim.includesObject?.unitCode &&
|
|
offerClaim.includesObject?.amountOfThisGood
|
|
) {
|
|
return displayAmount(
|
|
offerClaim.includesObject.unitCode,
|
|
offerClaim.includesObject.amountOfThisGood,
|
|
);
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Extracts claim description
|
|
* Falls back to name if no description available
|
|
*
|
|
* @param claim - Claim to get description from
|
|
* @returns Description string or empty string
|
|
*/
|
|
claimDescription(claim: GenericVerifiableCredential) {
|
|
return claim.claim.name || claim.claim.description || "";
|
|
}
|
|
|
|
/**
|
|
* Prompts user to confirm visibility change for a contact
|
|
*
|
|
* @param contact - Contact to modify visibility for
|
|
* @param visibility - New visibility state to set
|
|
*/
|
|
async confirmSetVisibility(contact: Contact, visibility: boolean) {
|
|
const visibilityPrompt = visibility
|
|
? "Are you sure you want to make your activity visible to them?"
|
|
: "Are you sure you want to hide all your activity from them?";
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Set Visibility",
|
|
text: visibilityPrompt,
|
|
onYes: async () => {
|
|
const success = await this.setVisibility(contact, visibility, true);
|
|
if (success) {
|
|
contact.seesMe = visibility; // didn't work inside setVisibility
|
|
}
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates contact visibility on server and local database
|
|
*
|
|
* @param contact - Contact to update visibility for
|
|
* @param visibility - New visibility state
|
|
* @param showSuccessAlert - Whether to show success notification
|
|
* @returns Boolean indicating success
|
|
*/
|
|
async setVisibility(
|
|
contact: Contact,
|
|
visibility: boolean,
|
|
showSuccessAlert: boolean,
|
|
) {
|
|
const result = await setVisibilityUtil(
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
db,
|
|
contact,
|
|
visibility,
|
|
);
|
|
if (result.success) {
|
|
//contact.seesMe = visibility; // why doesn't it affect the UI from here?
|
|
//console.log("Set result & seesMe", result, contact.seesMe, contact.did);
|
|
if (showSuccessAlert) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Visibility Set",
|
|
text:
|
|
(contact.name || "That user") +
|
|
" can " +
|
|
(visibility ? "" : "not ") +
|
|
"see your activity.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
return true;
|
|
} else {
|
|
logger.error("Got strange result from setting visibility:", result);
|
|
const message =
|
|
(result.error as string) || "Could not set visibility on the server.";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Setting Visibility",
|
|
text: message,
|
|
},
|
|
5000,
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks current visibility status of contact on server
|
|
* Updates local database with current status
|
|
*
|
|
* @param contact - Contact to check visibility for
|
|
*/
|
|
async checkVisibility(contact: Contact) {
|
|
const url =
|
|
this.apiServer +
|
|
"/api/report/canDidExplicitlySeeMe?did=" +
|
|
encodeURIComponent(contact.did);
|
|
const headers = await getHeaders(this.activeDid);
|
|
if (!headers["Authorization"]) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "No Identity",
|
|
text: "There is no identity to use to check visibility.",
|
|
},
|
|
3000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const resp = await this.axios.get(url, { headers });
|
|
if (resp.status === 200) {
|
|
const visibility = resp.data;
|
|
contact.seesMe = visibility;
|
|
//console.log("Visi check:", visibility, contact.seesMe, contact.did);
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
await platformService.dbExec(
|
|
"UPDATE contacts SET seesMe = ? WHERE did = ?",
|
|
[visibility, contact.did],
|
|
);
|
|
if (USE_DEXIE_DB) {
|
|
await db.contacts.update(contact.did, { seesMe: visibility });
|
|
}
|
|
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "info",
|
|
title: "Visibility Refreshed",
|
|
text:
|
|
libsUtil.nameForContact(contact, true) +
|
|
" can " +
|
|
(visibility ? "" : "not ") +
|
|
"see your activity.",
|
|
},
|
|
3000,
|
|
);
|
|
} else {
|
|
logger.error("Got bad server response checking visibility:", resp);
|
|
const message = resp.data.error?.message || "Got bad server response.";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Checking Visibility",
|
|
text: message,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
logger.error("Caught error from request to check visibility:", err);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Checking Visibility",
|
|
text: "Check connectivity and try again.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.dialog-overlay {
|
|
z-index: 50;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 1.5rem;
|
|
}
|
|
.dialog {
|
|
background-color: white;
|
|
padding: 1rem;
|
|
border-radius: 0.5rem;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
}
|
|
</style>
|
|
|