|
|
|
<template>
|
|
|
|
<QuickNav selected="Contacts"></QuickNav>
|
|
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
|
|
<!-- Heading -->
|
|
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
|
|
Your Contacts
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
<div class="flex justify-between py-2">
|
|
|
|
<span />
|
|
|
|
<span>
|
|
|
|
<a
|
|
|
|
href="/help-onboarding"
|
|
|
|
target="_blank"
|
|
|
|
class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
|
|
|
|
>
|
|
|
|
Onboarding Guide
|
|
|
|
</a>
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- New Contact -->
|
|
|
|
<div class="mt-4 mb-4 flex items-stretch">
|
|
|
|
<router-link
|
|
|
|
:to="{ name: 'contact-qr' }"
|
|
|
|
class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
|
|
|
>
|
|
|
|
<fa icon="qrcode" class="fa-fw text-2xl" />
|
|
|
|
</router-link>
|
|
|
|
<textarea
|
|
|
|
type="text"
|
|
|
|
placeholder="URL or DID, Name, Public Key, Next Public Key Hash"
|
|
|
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
|
|
|
|
v-model="contactInput"
|
|
|
|
/>
|
|
|
|
<button
|
|
|
|
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
|
|
|
@click="onClickNewContact()"
|
|
|
|
>
|
|
|
|
<fa icon="plus" class="fa-fw"></fa>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="w-full text-right">
|
|
|
|
<button
|
|
|
|
href=""
|
|
|
|
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
|
|
|
|
@click="toggleShowContactAmounts()"
|
|
|
|
>
|
|
|
|
{{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<div class="flex justify-between mt-1" v-if="showGiveNumbers">
|
|
|
|
<div class="w-full text-right">
|
|
|
|
In the following, only the most recent hours are included. To see more,
|
|
|
|
click
|
|
|
|
<span
|
|
|
|
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 px-1 py-1 rounded-md"
|
|
|
|
>
|
|
|
|
<fa icon="file-lines" class="fa-fw" />
|
|
|
|
</span>
|
|
|
|
<br />
|
|
|
|
<button
|
|
|
|
href=""
|
|
|
|
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
|
|
|
|
v-bind:class="showGiveAmountsClassNames()"
|
|
|
|
@click="toggleShowGiveTotals()"
|
|
|
|
>
|
|
|
|
{{
|
|
|
|
showGiveTotals
|
|
|
|
? "Totals"
|
|
|
|
: showGiveConfirmed
|
|
|
|
? "Confirmed Amounts"
|
|
|
|
: "Unconfirmed Amounts"
|
|
|
|
}}
|
|
|
|
<fa icon="left-right" class="fa-fw" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Results List -->
|
|
|
|
<ul v-if="contacts.length > 0" class="border-t border-slate-300">
|
|
|
|
<li
|
|
|
|
class="border-b border-slate-300 pt-2.5 pb-4"
|
|
|
|
v-for="contact in contacts"
|
|
|
|
:key="contact.did"
|
|
|
|
>
|
|
|
|
<div class="grow overflow-hidden">
|
|
|
|
<h2 class="text-base font-semibold">
|
|
|
|
<EntityIcon
|
|
|
|
:contact="contact"
|
|
|
|
:iconSize="24"
|
|
|
|
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
|
|
|
|
@click="showLargeIdenticon = contact"
|
|
|
|
/>
|
|
|
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
|
|
|
<button
|
|
|
|
@click="
|
|
|
|
contactEdit = contact;
|
|
|
|
contactNewName = contact.name || '';
|
|
|
|
"
|
|
|
|
title="Edit"
|
|
|
|
>
|
|
|
|
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1"></fa>
|
|
|
|
</button>
|
|
|
|
<router-link
|
|
|
|
:to="{
|
|
|
|
path: '/did/' + encodeURIComponent(contact.did),
|
|
|
|
}"
|
|
|
|
title="See more about this DID"
|
|
|
|
>
|
|
|
|
<fa icon="circle-info" class="text-blue-500 ml-4" />
|
|
|
|
</router-link>
|
|
|
|
</h2>
|
|
|
|
<div class="text-sm truncate">
|
|
|
|
{{ contact.did }}
|
|
|
|
<button
|
|
|
|
@click="
|
|
|
|
libsUtil.doCopyTwoSecRedo(
|
|
|
|
contact.did,
|
|
|
|
() => (showDidCopy = !showDidCopy),
|
|
|
|
)
|
|
|
|
"
|
|
|
|
class="ml-2 mr-2"
|
|
|
|
>
|
|
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
|
|
</button>
|
|
|
|
<span v-show="showDidCopy">Copied DID</span>
|
|
|
|
</div>
|
|
|
|
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
|
|
|
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
|
|
|
</div>
|
|
|
|
<div class="text-sm truncate" v-if="contact.nextPubKeyHashB64">
|
|
|
|
Next Public Key Hash (base 64):
|
|
|
|
{{ contact.nextPubKeyHashB64 }}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
|
|
|
<div v-if="activeDid">
|
|
|
|
<button
|
|
|
|
v-if="contact.seesMe"
|
|
|
|
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"
|
|
|
|
@click="confirmSetVisibility(contact, false)"
|
|
|
|
title="They can see you"
|
|
|
|
>
|
|
|
|
<fa icon="eye" class="fa-fw" />
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
v-else
|
|
|
|
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"
|
|
|
|
@click="confirmSetVisibility(contact, true)"
|
|
|
|
title="They cannot see you"
|
|
|
|
>
|
|
|
|
<fa icon="eye-slash" class="fa-fw" />
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
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"
|
|
|
|
@click="checkVisibility(contact)"
|
|
|
|
title="Check Visibility"
|
|
|
|
v-if="activeDid"
|
|
|
|
>
|
|
|
|
<fa icon="rotate" class="fa-fw" />
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
@click="confirmRegister(contact)"
|
|
|
|
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 px-2 py-1.5 rounded-md"
|
|
|
|
v-if="activeDid"
|
|
|
|
title="Registration"
|
|
|
|
>
|
|
|
|
<fa
|
|
|
|
v-if="contact.registered"
|
|
|
|
icon="person-circle-check"
|
|
|
|
class="fa-fw"
|
|
|
|
/>
|
|
|
|
<fa v-else icon="person-circle-question" class="fa-fw" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<button
|
|
|
|
@click="confirmDeleteContact(contact)"
|
|
|
|
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 px-2 py-1.5 rounded-md"
|
|
|
|
title="Delete"
|
|
|
|
>
|
|
|
|
<fa icon="trash-can" class="fa-fw" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<div
|
|
|
|
v-if="showGiveNumbers && contact.did != activeDid"
|
|
|
|
class="ml-auto flex gap-1.5"
|
|
|
|
>
|
|
|
|
<button
|
|
|
|
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
|
|
|
|
@click="confirmShowGiftedDialog(activeDid, contact.did)"
|
|
|
|
:title="givenByMeDescriptions[contact.did] || ''"
|
|
|
|
>
|
|
|
|
To:
|
|
|
|
<br />
|
|
|
|
{{
|
|
|
|
/* eslint-disable prettier/prettier */
|
|
|
|
this.showGiveTotals
|
|
|
|
? ((givenByMeConfirmed[contact.did] || 0)
|
|
|
|
+ (givenByMeUnconfirmed[contact.did] || 0))
|
|
|
|
: this.showGiveConfirmed
|
|
|
|
? (givenByMeConfirmed[contact.did] || 0)
|
|
|
|
: (givenByMeUnconfirmed[contact.did] || 0)
|
|
|
|
/* eslint-enable prettier/prettier */
|
|
|
|
}}
|
|
|
|
<br />
|
|
|
|
<fa icon="plus" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button
|
|
|
|
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
|
|
|
|
@click="confirmShowGiftedDialog(contact.did, this.activeDid)"
|
|
|
|
:title="givenToMeDescriptions[contact.did] || ''"
|
|
|
|
>
|
|
|
|
From:
|
|
|
|
<br />
|
|
|
|
{{
|
|
|
|
/* eslint-disable prettier/prettier */
|
|
|
|
this.showGiveTotals
|
|
|
|
? ((givenToMeConfirmed[contact.did] || 0)
|
|
|
|
+ (givenToMeUnconfirmed[contact.did] || 0))
|
|
|
|
: this.showGiveConfirmed
|
|
|
|
? (givenToMeConfirmed[contact.did] || 0)
|
|
|
|
: (givenToMeUnconfirmed[contact.did] || 0)
|
|
|
|
/* eslint-enable prettier/prettier */
|
|
|
|
}}
|
|
|
|
<br />
|
|
|
|
<fa icon="plus" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button
|
|
|
|
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-blue-400"
|
|
|
|
@click="openOfferDialog(contact.did)"
|
|
|
|
>
|
|
|
|
Offer
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<router-link
|
|
|
|
:to="{
|
|
|
|
name: 'contact-amounts',
|
|
|
|
query: { contactDid: contact.did },
|
|
|
|
}"
|
|
|
|
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-slate-400"
|
|
|
|
title="See more given activity"
|
|
|
|
>
|
|
|
|
<fa icon="file-lines" class="fa-fw" />
|
|
|
|
</router-link>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
<p v-else>There are no contacts.</p>
|
|
|
|
|
|
|
|
<GiftedDialog ref="customGivenDialog" />
|
|
|
|
<OfferDialog ref="customOfferDialog" />
|
|
|
|
|
|
|
|
<div v-if="showLargeIdenticon" 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
|
|
|
|
:contact="showLargeIdenticon"
|
|
|
|
:iconSize="512"
|
|
|
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
|
|
|
@click="showLargeIdenticon = undefined"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="contactEdit !== null" class="dialog-overlay">
|
|
|
|
<div class="dialog">
|
|
|
|
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
|
|
|
placeholder="Name"
|
|
|
|
v-model="contactNewName"
|
|
|
|
/>
|
|
|
|
<div class="flex justify-between">
|
|
|
|
<button
|
|
|
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
|
|
|
@click="onClickSaveName(contactEdit, contactNewName)"
|
|
|
|
>
|
|
|
|
<fa icon="save" />
|
|
|
|
</button>
|
|
|
|
<span class="inline-block w-2" />
|
|
|
|
<button
|
|
|
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
|
|
|
@click="onClickCancelName()"
|
|
|
|
>
|
|
|
|
<fa icon="ban" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
import { AxiosError } from "axios";
|
|
|
|
import { IndexableType } from "dexie";
|
|
|
|
import * as R from "ramda";
|
|
|
|
import { IIdentifier } from "@veramo/core";
|
|
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
|
|
import { Router } from "vue-router";
|
|
|
|
|
|
|
|
import { AppString, NotificationIface } from "@/constants/app";
|
|
|
|
import { accountsDB, db } from "@/db/index";
|
|
|
|
import { Contact } from "@/db/tables/contacts";
|
|
|
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
|
|
|
import { accessToken, getContactPayloadFromJwtUrl } from "@/libs/crypto";
|
|
|
|
import {
|
|
|
|
CONTACT_CSV_HEADER,
|
|
|
|
CONTACT_URL_PREFIX,
|
|
|
|
GiverReceiverInputInfo,
|
|
|
|
GiveSummaryRecord,
|
|
|
|
isDid,
|
|
|
|
register,
|
|
|
|
setVisibilityUtil,
|
|
|
|
} from "@/libs/endorserServer";
|
|
|
|
import * as libsUtil from "@/libs/util";
|
|
|
|
import QuickNav from "@/components/QuickNav.vue";
|
|
|
|
import EntityIcon from "@/components/EntityIcon.vue";
|
|
|
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
|
|
|
import OfferDialog from "@/components/OfferDialog.vue";
|
|
|
|
import { Account } from "@/db/tables/accounts";
|
|
|
|
|
|
|
|
import { Buffer } from "buffer/";
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
components: { GiftedDialog, EntityIcon, OfferDialog, QuickNav },
|
|
|
|
})
|
|
|
|
export default class ContactsView extends Vue {
|
|
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
|
|
|
|
activeDid = "";
|
|
|
|
apiServer = "";
|
|
|
|
contacts: Array<Contact> = [];
|
|
|
|
contactInput = "";
|
|
|
|
contactEdit: Contact | null = null;
|
|
|
|
contactNewName = "";
|
|
|
|
// { "did:...": concatenated-descriptions } entry for each contact
|
|
|
|
givenByMeDescriptions: Record<string, string> = {};
|
|
|
|
// { "did:...": amount } entry for each contact
|
|
|
|
givenByMeConfirmed: Record<string, number> = {};
|
|
|
|
// { "did:...": amount } entry for each contact
|
|
|
|
givenByMeUnconfirmed: Record<string, number> = {};
|
|
|
|
// { "did:...": concatenated-descriptions } entry for each contact
|
|
|
|
givenToMeDescriptions: Record<string, string> = {};
|
|
|
|
// { "did:...": amount } entry for each contact
|
|
|
|
givenToMeConfirmed: Record<string, number> = {};
|
|
|
|
// { "did:...": amount } entry for each contact
|
|
|
|
givenToMeUnconfirmed: Record<string, number> = {};
|
|
|
|
hideRegisterPromptOnNewContact = false;
|
|
|
|
isRegistered = false;
|
|
|
|
showDidCopy = false;
|
|
|
|
showGiveNumbers = false;
|
|
|
|
showGiveTotals = true;
|
|
|
|
showGiveConfirmed = true;
|
|
|
|
showLargeIdenticon?: Contact;
|
|
|
|
|
|
|
|
AppString = AppString;
|
|
|
|
libsUtil = libsUtil;
|
|
|
|
|
|
|
|
async created() {
|
|
|
|
await db.open();
|
|
|
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
|
|
|
this.activeDid = settings?.activeDid || "";
|
|
|
|
this.apiServer = settings?.apiServer || "";
|
|
|
|
this.isRegistered = !!settings?.isRegistered;
|
|
|
|
|
|
|
|
this.showGiveNumbers = !!settings?.showContactGivesInline;
|
|
|
|
this.hideRegisterPromptOnNewContact =
|
|
|
|
!!settings?.hideRegisterPromptOnNewContact;
|
|
|
|
|
|
|
|
if (this.showGiveNumbers) {
|
|
|
|
this.loadGives();
|
|
|
|
}
|
|
|
|
|
|
|
|
// .orderBy("name") wouldn't retrieve any entries with a blank name
|
|
|
|
// .toCollection.sortBy("name") didn't sort in an order I understood
|
|
|
|
const baseContacts = await db.contacts.toArray();
|
|
|
|
this.contacts = baseContacts.sort((a, b) =>
|
|
|
|
(a.name || "").localeCompare(b.name || ""),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
danger(message: string, title: string = "Error", timeout = 5000) {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: title,
|
|
|
|
text: message,
|
|
|
|
},
|
|
|
|
timeout,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getIdentity(activeDid: string): Promise<IIdentifier> {
|
|
|
|
await accountsDB.open();
|
|
|
|
const accounts = await accountsDB.accounts.toArray();
|
|
|
|
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
|
|
|
|
const identity = JSON.parse(account?.identity || "null");
|
|
|
|
|
|
|
|
if (!identity) {
|
|
|
|
throw new Error(
|
|
|
|
"Attempted to load Give records with no identifier available.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return identity;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getHeaders(identity: IIdentifier) {
|
|
|
|
const token = await accessToken(identity);
|
|
|
|
const headers = {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
Authorization: "Bearer " + token,
|
|
|
|
};
|
|
|
|
return headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getHeadersAndIdentity(activeDid: string) {
|
|
|
|
const identity = await this.getIdentity(activeDid);
|
|
|
|
const headers = await this.getHeaders(identity);
|
|
|
|
|
|
|
|
return { headers, identity };
|
|
|
|
}
|
|
|
|
|
|
|
|
async loadGives() {
|
|
|
|
if (!this.activeDid) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleResponse = (
|
|
|
|
resp: { status: number; data: { data: GiveSummaryRecord[] } },
|
|
|
|
descriptions: Record<string, string>,
|
|
|
|
confirmed: Record<string, number>,
|
|
|
|
unconfirmed: Record<string, number>,
|
|
|
|
useRecipient: boolean,
|
|
|
|
) => {
|
|
|
|
if (resp.status === 200) {
|
|
|
|
const allData = resp.data.data;
|
|
|
|
for (const give of allData) {
|
|
|
|
const otherDid = useRecipient ? give.recipientDid : give.agentDid;
|
|
|
|
if (give.unit === "HUR") {
|
|
|
|
if (give.amountConfirmed) {
|
|
|
|
const prevAmount = confirmed[otherDid] || 0;
|
|
|
|
confirmed[otherDid] = prevAmount + give.amount;
|
|
|
|
} else {
|
|
|
|
const prevAmount = unconfirmed[otherDid] || 0;
|
|
|
|
unconfirmed[otherDid] = prevAmount + give.amount;
|
|
|
|
}
|
|
|
|
if (!descriptions[otherDid] && give.description) {
|
|
|
|
descriptions[otherDid] = give.description;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.error(
|
|
|
|
"Got bad response status & data of",
|
|
|
|
resp.status,
|
|
|
|
resp.data,
|
|
|
|
);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Retrieval Error",
|
|
|
|
text:
|
|
|
|
"Got an error retrieving your " +
|
|
|
|
(useRecipient ? "given" : "received") +
|
|
|
|
" data from the server.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
const { headers } = await this.getHeadersAndIdentity(this.activeDid);
|
|
|
|
const givenByUrl =
|
|
|
|
this.apiServer +
|
|
|
|
"/api/v2/report/gives?agentDid=" +
|
|
|
|
encodeURIComponent(this.activeDid);
|
|
|
|
const givenToUrl =
|
|
|
|
this.apiServer +
|
|
|
|
"/api/v2/report/gives?recipientDid=" +
|
|
|
|
encodeURIComponent(this.activeDid);
|
|
|
|
|
|
|
|
const [givenByMeResp, givenToMeResp] = await Promise.all([
|
|
|
|
this.axios.get(givenByUrl, { headers }),
|
|
|
|
this.axios.get(givenToUrl, { headers }),
|
|
|
|
]);
|
|
|
|
|
|
|
|
const givenByMeDescriptions = {};
|
|
|
|
const givenByMeConfirmed = {};
|
|
|
|
const givenByMeUnconfirmed = {};
|
|
|
|
handleResponse(
|
|
|
|
givenByMeResp,
|
|
|
|
givenByMeDescriptions,
|
|
|
|
givenByMeConfirmed,
|
|
|
|
givenByMeUnconfirmed,
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
this.givenByMeDescriptions = givenByMeDescriptions;
|
|
|
|
this.givenByMeConfirmed = givenByMeConfirmed;
|
|
|
|
this.givenByMeUnconfirmed = givenByMeUnconfirmed;
|
|
|
|
|
|
|
|
const givenToMeDescriptions = {};
|
|
|
|
const givenToMeConfirmed = {};
|
|
|
|
const givenToMeUnconfirmed = {};
|
|
|
|
handleResponse(
|
|
|
|
givenToMeResp,
|
|
|
|
givenToMeDescriptions,
|
|
|
|
givenToMeConfirmed,
|
|
|
|
givenToMeUnconfirmed,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
this.givenToMeDescriptions = givenToMeDescriptions;
|
|
|
|
this.givenToMeConfirmed = givenToMeConfirmed;
|
|
|
|
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error loading gives", error);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Load Error",
|
|
|
|
text: "Got an error loading your gives.",
|
|
|
|
},
|
|
|
|
5000,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async onClickNewContact(): Promise<void> {
|
|
|
|
if (!this.contactInput) {
|
|
|
|
this.danger("There was no contact info to add.", "No Contact");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
|
|
|
await this.addContactFromScan(this.contactInput);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.contactInput.startsWith(CONTACT_CSV_HEADER)) {
|
|
|
|
const lines = this.contactInput.split(/\n/);
|
|
|
|
const lineAdded = [];
|
|
|
|
for (const line of lines) {
|
|
|
|
if (!line.trim() || line.startsWith(CONTACT_CSV_HEADER)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
lineAdded.push(this.addContactFromEndorserMobileLine(line));
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
await Promise.all(lineAdded);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "success",
|
|
|
|
title: "Contacts Added",
|
|
|
|
text: "Each contact was added. Nothing was sent to the server.",
|
|
|
|
},
|
|
|
|
3000, // keeping it up so that the "visibility" message is seen
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
this.danger("An error occurred. Some contacts may have been added.");
|
|
|
|
}
|
|
|
|
|
|
|
|
// .orderBy("name") wouldn't retrieve any entries with a blank name
|
|
|
|
// .toCollection.sortBy("name") didn't sort in an order I understood
|
|
|
|
const baseContacts = await db.contacts.toArray();
|
|
|
|
this.contacts = baseContacts.sort((a, b) =>
|
|
|
|
(a.name || "").localeCompare(b.name || ""),
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let did = this.contactInput;
|
|
|
|
let name, publicKeyInput, nextPublicKeyHashInput;
|
|
|
|
const commaPos1 = this.contactInput.indexOf(",");
|
|
|
|
if (commaPos1 > -1) {
|
|
|
|
did = this.contactInput.substring(0, commaPos1).trim();
|
|
|
|
name = this.contactInput.substring(commaPos1 + 1).trim();
|
|
|
|
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
|
|
|
|
if (commaPos2 > -1) {
|
|
|
|
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
|
|
|
publicKeyInput = this.contactInput.substring(commaPos2 + 1).trim();
|
|
|
|
const commaPos3 = this.contactInput.indexOf(",", commaPos2 + 1);
|
|
|
|
if (commaPos3 > -1) {
|
|
|
|
publicKeyInput = this.contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
|
|
|
|
nextPublicKeyHashInput = this.contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// help with potential mistakes while this sharing requires copy-and-paste
|
|
|
|
let publicKeyBase64 = publicKeyInput;
|
|
|
|
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
|
|
|
// it must be all hex (compressed public key), so convert
|
|
|
|
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
|
|
|
}
|
|
|
|
let nextPubKeyHashB64 = nextPublicKeyHashInput;
|
|
|
|
if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) {
|
|
|
|
// it must be all hex (compressed public key), so convert
|
|
|
|
nextPubKeyHashB64 = Buffer.from(nextPubKeyHashB64, "hex").toString("base64"); // eslint-disable-line prettier/prettier
|
|
|
|
}
|
|
|
|
const newContact = {
|
|
|
|
did,
|
|
|
|
name,
|
|
|
|
publicKeyBase64,
|
|
|
|
nextPubKeyHashB64: nextPubKeyHashB64,
|
|
|
|
};
|
|
|
|
await this.addContact(newContact);
|
|
|
|
}
|
|
|
|
|
|
|
|
async addContactFromEndorserMobileLine(line: string): Promise<IndexableType> {
|
|
|
|
// Note that Endorser Mobile puts name first, then did, etc.
|
|
|
|
let name = line;
|
|
|
|
let did = "";
|
|
|
|
let publicKeyInput, seesMe, registered;
|
|
|
|
const commaPos1 = line.indexOf(",");
|
|
|
|
if (commaPos1 > -1) {
|
|
|
|
name = line.substring(0, commaPos1).trim();
|
|
|
|
did = line.substring(commaPos1 + 1).trim();
|
|
|
|
const commaPos2 = line.indexOf(",", commaPos1 + 1);
|
|
|
|
if (commaPos2 > -1) {
|
|
|
|
did = line.substring(commaPos1 + 1, commaPos2).trim();
|
|
|
|
publicKeyInput = line.substring(commaPos2 + 1).trim();
|
|
|
|
const commaPos3 = line.indexOf(",", commaPos2 + 1);
|
|
|
|
if (commaPos3 > -1) {
|
|
|
|
publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
|
|
|
|
seesMe = line.substring(commaPos3 + 1).trim() == "true";
|
|
|
|
const commaPos4 = line.indexOf(",", commaPos3 + 1);
|
|
|
|
if (commaPos4 > -1) {
|
|
|
|
seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
|
|
|
|
registered = line.substring(commaPos4 + 1).trim() == "true";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// help with potential mistakes while this sharing requires copy-and-paste
|
|
|
|
let publicKeyBase64 = publicKeyInput;
|
|
|
|
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
|
|
|
// it must be all hex (compressed public key), so convert
|
|
|
|
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
|
|
|
}
|
|
|
|
const newContact = {
|
|
|
|
did,
|
|
|
|
name,
|
|
|
|
publicKeyBase64,
|
|
|
|
seesMe,
|
|
|
|
registered,
|
|
|
|
};
|
|
|
|
return db.contacts.add(newContact);
|
|
|
|
}
|
|
|
|
|
|
|
|
async addContactFromScan(url: string): Promise<void> {
|
|
|
|
const payload = getContactPayloadFromJwtUrl(url);
|
|
|
|
if (!payload) {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "No Contact Info",
|
|
|
|
text: "The contact info could not be parsed.",
|
|
|
|
},
|
|
|
|
3000,
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
return this.addContact({
|
|
|
|
did: payload.iss,
|
|
|
|
name: payload.own.name,
|
|
|
|
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
|
|
|
|
profileImageUrl: payload.own.profileImageUrl,
|
|
|
|
publicKeyBase64: payload.own.publicEncKey,
|
|
|
|
registered: payload.own.registered,
|
|
|
|
} as Contact);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async addContact(newContact: Contact) {
|
|
|
|
if (!newContact.did) {
|
|
|
|
this.danger("Cannot add a contact without a DID.", "Incomplete Contact");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!isDid(newContact.did)) {
|
|
|
|
this.danger("The DID must begin with 'did:'", "Invalid DID");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return db.contacts
|
|
|
|
.add(newContact)
|
|
|
|
.then(() => {
|
|
|
|
const allContacts = this.contacts.concat([newContact]);
|
|
|
|
this.contacts = R.sort(
|
|
|
|
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
|
|
|
allContacts,
|
|
|
|
);
|
|
|
|
let addedMessage;
|
|
|
|
if (this.activeDid) {
|
|
|
|
this.setVisibility(newContact, true, false);
|
|
|
|
newContact.seesMe = true; // didn't work inside setVisibility
|
|
|
|
addedMessage =
|
|
|
|
"They were added, and your activity is visible to them.";
|
|
|
|
} else {
|
|
|
|
addedMessage = "They were added.";
|
|
|
|
}
|
|
|
|
this.contactInput = "";
|
|
|
|
if (this.isRegistered) {
|
|
|
|
if (!this.hideRegisterPromptOnNewContact && !newContact.registered) {
|
|
|
|
setTimeout(() => {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "modal",
|
|
|
|
type: "confirm",
|
|
|
|
title: "Register",
|
|
|
|
text: "Do you want to register them?",
|
|
|
|
onCancel: async (stopAsking: boolean) => {
|
|
|
|
if (stopAsking) {
|
|
|
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
|
|
hideRegisterPromptOnNewContact: stopAsking,
|
|
|
|
});
|
|
|
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onNo: async (stopAsking: boolean) => {
|
|
|
|
if (stopAsking) {
|
|
|
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
|
|
hideRegisterPromptOnNewContact: stopAsking,
|
|
|
|
});
|
|
|
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onYes: async () => {
|
|
|
|
await this.register(newContact);
|
|
|
|
},
|
|
|
|
promptToStopAsking: true,
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}, 500);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "success",
|
|
|
|
title: "Contact Added",
|
|
|
|
text: addedMessage,
|
|
|
|
},
|
|
|
|
3000,
|
|
|
|
);
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error("Error when adding contact to storage:", err);
|
|
|
|
let message = "An error prevented this import.";
|
|
|
|
if (
|
|
|
|
err.message?.indexOf("Key already exists in the object store.") > -1
|
|
|
|
) {
|
|
|
|
message =
|
|
|
|
"A contact with that DID is already in your contact list. Edit them directly below.";
|
|
|
|
}
|
|
|
|
if (err.name === "ConstraintError") {
|
|
|
|
message +=
|
|
|
|
" Check that the contact doesn't conflict with any you already have.";
|
|
|
|
}
|
|
|
|
this.danger(message, "Contact Not Added", -1);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// prompt with confirmation if they want to delete a contact
|
|
|
|
confirmDeleteContact(contact: Contact) {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "modal",
|
|
|
|
type: "confirm",
|
|
|
|
title: "Delete",
|
|
|
|
text:
|
|
|
|
"Are you sure you want to remove " +
|
|
|
|
this.nameForDid(this.contacts, contact.did) +
|
|
|
|
" with DID " +
|
|
|
|
contact.did +
|
|
|
|
" from your contact list?",
|
|
|
|
onYes: async () => {
|
|
|
|
await this.deleteContact(contact);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async deleteContact(contact: Contact) {
|
|
|
|
await db.open();
|
|
|
|
await db.contacts.delete(contact.did);
|
|
|
|
this.contacts = R.without([contact], this.contacts);
|
|
|
|
}
|
|
|
|
|
|
|
|
// confirm to register a new contact
|
|
|
|
async confirmRegister(contact: Contact) {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "modal",
|
|
|
|
type: "confirm",
|
|
|
|
title: "Register",
|
|
|
|
text:
|
|
|
|
"Are you sure you want to register " +
|
|
|
|
this.nameForDid(this.contacts, contact.did) +
|
|
|
|
(contact.registered
|
|
|
|
? " -- especially since they are already marked as registered"
|
|
|
|
: "") +
|
|
|
|
"?",
|
|
|
|
onYes: async () => {
|
|
|
|
await this.register(contact);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
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) {
|
|
|
|
console.error("Error when registering:", error);
|
|
|
|
let userMessage = "There was an error. See logs for more info.";
|
|
|
|
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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 () => {
|
|
|
|
await this.setVisibility(contact, visibility, true);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async setVisibility(
|
|
|
|
contact: Contact,
|
|
|
|
visibility: boolean,
|
|
|
|
showSuccessAlert: boolean,
|
|
|
|
) {
|
|
|
|
const result = await setVisibilityUtil(
|
|
|
|
this.activeDid,
|
|
|
|
this.apiServer,
|
|
|
|
this.axios,
|
|
|
|
db,
|
|
|
|
contact,
|
|
|
|
visibility,
|
|
|
|
);
|
|
|
|
if (result.success) {
|
|
|
|
if (showSuccessAlert) {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "success",
|
|
|
|
title: "Visibility Set",
|
|
|
|
text:
|
|
|
|
(contact.name || "That user") +
|
|
|
|
" can " +
|
|
|
|
(visibility ? "" : "not ") +
|
|
|
|
"see your activity.",
|
|
|
|
},
|
|
|
|
3000,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else if (result.error) {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error Setting Visibility",
|
|
|
|
text: result.error as string,
|
|
|
|
},
|
|
|
|
5000,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
console.error("Got strange result from setting visibility:", result);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async checkVisibility(contact: Contact) {
|
|
|
|
const url =
|
|
|
|
this.apiServer +
|
|
|
|
"/api/report/canDidExplicitlySeeMe?did=" +
|
|
|
|
encodeURIComponent(contact.did);
|
|
|
|
const identity = await this.getIdentity(this.activeDid);
|
|
|
|
const headers = await this.getHeaders(identity);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const resp = await this.axios.get(url, { headers });
|
|
|
|
if (resp.status === 200) {
|
|
|
|
const visibility = resp.data;
|
|
|
|
contact.seesMe = visibility;
|
|
|
|
console.log(
|
|
|
|
"Visibility checked:",
|
|
|
|
visibility,
|
|
|
|
contact.did,
|
|
|
|
contact.name,
|
|
|
|
); // eslint-disable-line no-console
|
|
|
|
console.log(this.contacts); // eslint-disable-line no-console
|
|
|
|
db.contacts.update(contact.did, { seesMe: visibility });
|
|
|
|
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "info",
|
|
|
|
title: "Visibility Refreshed",
|
|
|
|
text:
|
|
|
|
this.nameForContact(contact, true) +
|
|
|
|
" can " +
|
|
|
|
(visibility ? "" : "not ") +
|
|
|
|
"see your activity.",
|
|
|
|
},
|
|
|
|
3000,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
console.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) {
|
|
|
|
console.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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private nameForDid(contacts: Array<Contact>, did: string): string {
|
|
|
|
if (did === this.activeDid) {
|
|
|
|
return "you";
|
|
|
|
}
|
|
|
|
const contact = R.find((con) => con.did == did, contacts);
|
|
|
|
return this.nameForContact(contact);
|
|
|
|
}
|
|
|
|
|
|
|
|
private nameForContact(contact?: Contact, capitalize?: boolean): string {
|
|
|
|
return (
|
|
|
|
(contact?.name as string) ||
|
|
|
|
(capitalize ? "This" : "this") + " unnamed user"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
confirmShowGiftedDialog(giverDid: string, recipientDid: string) {
|
|
|
|
// if they have unconfirmed amounts, ask to confirm those
|
|
|
|
if (
|
|
|
|
recipientDid === this.activeDid &&
|
|
|
|
this.givenToMeUnconfirmed[giverDid] > 0
|
|
|
|
) {
|
|
|
|
const isAre = this.givenToMeUnconfirmed[giverDid] == 1 ? "is" : "are";
|
|
|
|
const hours = this.givenToMeUnconfirmed[giverDid] == 1 ? "hour" : "hours";
|
|
|
|
const message =
|
|
|
|
"There " +
|
|
|
|
isAre +
|
|
|
|
" " +
|
|
|
|
this.givenToMeUnconfirmed[giverDid] +
|
|
|
|
" unconfirmed " +
|
|
|
|
hours +
|
|
|
|
" from them." +
|
|
|
|
" Would you like to confirm some of those hours?";
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "modal",
|
|
|
|
type: "confirm",
|
|
|
|
title: "Delete",
|
|
|
|
text: message,
|
|
|
|
onNo: async () => {
|
|
|
|
this.showGiftedDialog(giverDid, recipientDid);
|
|
|
|
},
|
|
|
|
onYes: async () => {
|
|
|
|
(this.$router as Router).push({
|
|
|
|
name: "contact-amounts",
|
|
|
|
query: { contactDid: giverDid },
|
|
|
|
});
|
|
|
|
},
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.showGiftedDialog(giverDid, recipientDid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private showGiftedDialog(giverDid: string, recipientDid: string) {
|
|
|
|
let giver: GiverReceiverInputInfo, receiver: GiverReceiverInputInfo;
|
|
|
|
if (giverDid) {
|
|
|
|
giver = {
|
|
|
|
did: giverDid,
|
|
|
|
name: this.nameForDid(this.contacts, giverDid),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
if (recipientDid) {
|
|
|
|
receiver = {
|
|
|
|
did: recipientDid,
|
|
|
|
name: this.nameForDid(this.contacts, recipientDid),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
let callback: (amount: number) => void;
|
|
|
|
let customTitle = "";
|
|
|
|
// choose whether to open dialog to user or from user
|
|
|
|
if (giverDid == this.activeDid) {
|
|
|
|
callback = (amount: number) => {
|
|
|
|
const newList = R.clone(this.givenByMeUnconfirmed);
|
|
|
|
newList[recipientDid] = (newList[recipientDid] || 0) + amount;
|
|
|
|
this.givenByMeUnconfirmed = newList;
|
|
|
|
};
|
|
|
|
customTitle = "Given to " + receiver.name;
|
|
|
|
} else {
|
|
|
|
// must be (recipientDid == this.activeDid)
|
|
|
|
callback = (amount: number) => {
|
|
|
|
const newList = R.clone(this.givenToMeUnconfirmed);
|
|
|
|
newList[giverDid] = (newList[giverDid] || 0) + amount;
|
|
|
|
this.givenToMeUnconfirmed = newList;
|
|
|
|
};
|
|
|
|
customTitle = "Received from " + giver.name;
|
|
|
|
}
|
|
|
|
(this.$refs.customGivenDialog as GiftedDialog).open(
|
|
|
|
giver,
|
|
|
|
receiver,
|
|
|
|
undefined as string,
|
|
|
|
customTitle,
|
|
|
|
callback,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
openOfferDialog(recipientDid: string) {
|
|
|
|
(this.$refs.customOfferDialog as OfferDialog).open(recipientDid);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async onClickCancelName() {
|
|
|
|
this.contactEdit = null;
|
|
|
|
this.contactNewName = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
private async onClickSaveName(contact: Contact, newName: string) {
|
|
|
|
contact.name = newName;
|
|
|
|
return db.contacts
|
|
|
|
.update(contact.did, { name: newName })
|
|
|
|
.then(() => (this.contactEdit = null));
|
|
|
|
}
|
|
|
|
|
|
|
|
public async toggleShowContactAmounts() {
|
|
|
|
const newShowValue = !this.showGiveNumbers;
|
|
|
|
try {
|
|
|
|
await db.open();
|
|
|
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
|
|
showContactGivesInline: newShowValue,
|
|
|
|
});
|
|
|
|
} catch (err) {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error Updating Contact Setting",
|
|
|
|
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
console.error(
|
|
|
|
"Telling user to try again after contact-amounts setting update because:",
|
|
|
|
err,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
this.showGiveNumbers = newShowValue;
|
|
|
|
if (
|
|
|
|
newShowValue &&
|
|
|
|
Object.keys(this.givenByMeDescriptions).length === 0 &&
|
|
|
|
Object.keys(this.givenByMeConfirmed).length === 0 &&
|
|
|
|
Object.keys(this.givenByMeUnconfirmed).length === 0 &&
|
|
|
|
Object.keys(this.givenToMeDescriptions).length === 0 &&
|
|
|
|
Object.keys(this.givenToMeConfirmed).length === 0 &&
|
|
|
|
Object.keys(this.givenToMeUnconfirmed).length === 0
|
|
|
|
) {
|
|
|
|
// assume we should load it all
|
|
|
|
this.loadGives();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public toggleShowGiveTotals() {
|
|
|
|
if (this.showGiveTotals) {
|
|
|
|
this.showGiveTotals = false;
|
|
|
|
this.showGiveConfirmed = true;
|
|
|
|
} else if (this.showGiveConfirmed) {
|
|
|
|
this.showGiveTotals = false; // stays the same
|
|
|
|
this.showGiveConfirmed = false;
|
|
|
|
} else {
|
|
|
|
this.showGiveTotals = true;
|
|
|
|
this.showGiveConfirmed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public showGiveAmountsClassNames() {
|
|
|
|
return {
|
|
|
|
"from-slate-400": this.showGiveTotals,
|
|
|
|
"to-slate-700": this.showGiveTotals,
|
|
|
|
"from-green-400": !this.showGiveTotals && this.showGiveConfirmed,
|
|
|
|
"to-green-700": !this.showGiveTotals && this.showGiveConfirmed,
|
|
|
|
"from-yellow-400": !this.showGiveTotals && !this.showGiveConfirmed,
|
|
|
|
"to-yellow-700": !this.showGiveTotals && !this.showGiveConfirmed,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
.dialog-overlay {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Tooltip, generated on "title" attributes on "fa" icons
|
|
|
|
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
|
|
|
*/
|
|
|
|
/* Tooltip container */
|
|
|
|
.tooltip {
|
|
|
|
position: relative;
|
|
|
|
display: inline-block;
|
|
|
|
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
|
|
|
}
|
|
|
|
/* Tooltip text */
|
|
|
|
.tooltip .tooltiptext {
|
|
|
|
visibility: hidden;
|
|
|
|
width: 200px;
|
|
|
|
background-color: black;
|
|
|
|
color: #fff;
|
|
|
|
text-align: center;
|
|
|
|
padding: 5px 0;
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
position: absolute;
|
|
|
|
z-index: 1;
|
|
|
|
}
|
|
|
|
/* How do we share with the above so code isn't duplicated? */
|
|
|
|
.tooltip .tooltiptext-left {
|
|
|
|
visibility: hidden;
|
|
|
|
width: 200px;
|
|
|
|
background-color: black;
|
|
|
|
color: #fff;
|
|
|
|
text-align: center;
|
|
|
|
padding: 5px 0;
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
position: absolute;
|
|
|
|
z-index: 1;
|
|
|
|
|
|
|
|
bottom: 0%;
|
|
|
|
right: 105%;
|
|
|
|
margin-left: -60px;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Show the tooltip text when you mouse over the tooltip container */
|
|
|
|
.tooltip:hover .tooltiptext {
|
|
|
|
visibility: visible;
|
|
|
|
}
|
|
|
|
.tooltip:hover .tooltiptext-left {
|
|
|
|
visibility: visible;
|
|
|
|
}
|
|
|
|
</style>
|