forked from jsnbuchanan/crowd-funder-for-time-pwa
Changes: - Move v-model directives before other attributes - Move v-bind directives before event handlers - Reorder attributes for better readability - Fix template attribute ordering across components - Improve eslint rules - add default vite config for testing (handles nostr error too) This follows Vue.js style guide recommendations for attribute ordering and improves template consistency.
1447 lines
48 KiB
Vue
1447 lines
48 KiB
Vue
<template>
|
|
<QuickNav selected="Contacts" />
|
|
<TopMessage />
|
|
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Heading -->
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
|
Your Contacts
|
|
</h1>
|
|
|
|
<div class="flex justify-between py-2 mt-8">
|
|
<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 id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
|
|
<span v-if="isRegistered" class="flex">
|
|
<router-link
|
|
:to="{ name: 'invite-one' }"
|
|
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
|
>
|
|
<font-awesome icon="envelope-open-text" class="fa-fw text-2xl" />
|
|
</router-link>
|
|
|
|
<button
|
|
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
|
@click="showOnboardMeetingDialog()"
|
|
>
|
|
<font-awesome icon="chair" class="fa-fw text-2xl" />
|
|
</button>
|
|
</span>
|
|
<span v-else class="flex">
|
|
<span
|
|
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"
|
|
>
|
|
<font-awesome
|
|
icon="envelope-open-text"
|
|
class="fa-fw text-2xl"
|
|
@click="
|
|
warning(
|
|
'You must get registered before you can create invites.',
|
|
'Not Registered',
|
|
)
|
|
"
|
|
/>
|
|
</span>
|
|
<span
|
|
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"
|
|
>
|
|
<font-awesome
|
|
icon="chair"
|
|
class="fa-fw text-2xl"
|
|
@click="
|
|
warning(
|
|
'You must get registered before you can initiate an onboarding meeting.',
|
|
'Not Registered',
|
|
)
|
|
"
|
|
/>
|
|
</span>
|
|
</span>
|
|
|
|
<router-link
|
|
:to="{ name: 'contact-qr' }"
|
|
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
|
>
|
|
<font-awesome icon="qrcode" class="fa-fw text-2xl" />
|
|
</router-link>
|
|
|
|
<textarea
|
|
v-model="contactInput"
|
|
type="text"
|
|
placeholder="New 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"
|
|
/>
|
|
<button
|
|
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
|
|
@click="onClickNewContact()"
|
|
>
|
|
<font-awesome icon="plus" class="fa-fw" />
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="contacts.length > 0" class="flex justify-between">
|
|
<div class="w-full text-left">
|
|
<div v-if="!showGiveNumbers">
|
|
<input
|
|
type="checkbox"
|
|
:checked="contactsSelected.length === contacts.length"
|
|
class="align-middle ml-2 h-6 w-6"
|
|
data-testId="contactCheckAllTop"
|
|
@click="
|
|
contactsSelected.length === contacts.length
|
|
? (contactsSelected = [])
|
|
: (contactsSelected = contacts.map((contact) => contact.did))
|
|
"
|
|
/>
|
|
<button
|
|
v-if="!showGiveNumbers"
|
|
href=""
|
|
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
|
|
:style="
|
|
contactsSelected.length > 0
|
|
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
|
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
|
"
|
|
data-testId="copySelectedContactsButtonTop"
|
|
@click="copySelectedContacts()"
|
|
>
|
|
Copy Selections
|
|
</button>
|
|
<button @click="showCopySelectionsInfo()">
|
|
<font-awesome
|
|
icon="circle-info"
|
|
class="text-xl text-blue-500 ml-4"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</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 Hours, Offer, etc" : "See Hours, Offer, etc"
|
|
}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div v-if="showGiveNumbers" class="flex justify-between mt-1">
|
|
<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"
|
|
>
|
|
<font-awesome 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"
|
|
:class="showGiveAmountsClassNames()"
|
|
@click="toggleShowGiveTotals()"
|
|
>
|
|
{{
|
|
showGiveTotals
|
|
? "Totals"
|
|
: showGiveConfirmed
|
|
? "Confirmed Amounts"
|
|
: "Unconfirmed Amounts"
|
|
}}
|
|
<font-awesome icon="left-right" class="fa-fw" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results List -->
|
|
<ul
|
|
v-if="contacts.length > 0"
|
|
id="listContacts"
|
|
class="border-t border-slate-300 mt-1"
|
|
>
|
|
<li
|
|
v-for="contact in filteredContacts()"
|
|
:key="contact.did"
|
|
class="border-b border-slate-300 pt-1 pb-1"
|
|
data-testId="contactListItem"
|
|
>
|
|
<div class="grow overflow-hidden">
|
|
<div class="flex items-center">
|
|
<EntityIcon
|
|
:contact="contact"
|
|
:icon-size="24"
|
|
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
|
|
@click="showLargeIdenticon = contact"
|
|
/>
|
|
|
|
<input
|
|
v-if="!showGiveNumbers"
|
|
type="checkbox"
|
|
:checked="contactsSelected.includes(contact.did)"
|
|
class="ml-2 h-6 w-6 flex-shrink-0"
|
|
data-testId="contactCheckOne"
|
|
@click="
|
|
contactsSelected.includes(contact.did)
|
|
? contactsSelected.splice(
|
|
contactsSelected.indexOf(contact.did),
|
|
1,
|
|
)
|
|
: contactsSelected.push(contact.did)
|
|
"
|
|
/>
|
|
|
|
<h2
|
|
class="text-base font-semibold ml-2 w-1/3 truncate flex-shrink-0"
|
|
>
|
|
{{ contactNameNonBreakingSpace(contact.name) }}
|
|
</h2>
|
|
|
|
<span>
|
|
<div class="flex items-center">
|
|
<router-link
|
|
:to="{
|
|
path: '/did/' + encodeURIComponent(contact.did),
|
|
}"
|
|
title="See more about this person"
|
|
>
|
|
<font-awesome
|
|
icon="circle-info"
|
|
class="text-xl text-blue-500 ml-4"
|
|
/>
|
|
</router-link>
|
|
|
|
<span class="ml-4 text-sm overflow-hidden">{{
|
|
libsUtil.shortDid(contact.did)
|
|
}}</span>
|
|
</div>
|
|
<div class="ml-4 text-sm">
|
|
{{ contact.notes }}
|
|
</div>
|
|
</span>
|
|
</div>
|
|
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
|
<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"
|
|
:title="givenToMeDescriptions[contact.did] || ''"
|
|
@click="confirmShowGiftedDialog(contact.did, activeDid)"
|
|
>
|
|
From:
|
|
<br />
|
|
{{
|
|
/* eslint-disable prettier/prettier */
|
|
showGiveTotals
|
|
? ((givenToMeConfirmed[contact.did] || 0)
|
|
+ (givenToMeUnconfirmed[contact.did] || 0))
|
|
: showGiveConfirmed
|
|
? (givenToMeConfirmed[contact.did] || 0)
|
|
: (givenToMeUnconfirmed[contact.did] || 0)
|
|
/* eslint-enable prettier/prettier */
|
|
}}
|
|
</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"
|
|
:title="givenByMeDescriptions[contact.did] || ''"
|
|
@click="confirmShowGiftedDialog(activeDid, contact.did)"
|
|
>
|
|
To:
|
|
<br />
|
|
{{
|
|
/* eslint-disable prettier/prettier */
|
|
showGiveTotals
|
|
? ((givenByMeConfirmed[contact.did] || 0)
|
|
+ (givenByMeUnconfirmed[contact.did] || 0))
|
|
: showGiveConfirmed
|
|
? (givenByMeConfirmed[contact.did] || 0)
|
|
: (givenByMeUnconfirmed[contact.did] || 0)
|
|
/* eslint-enable prettier/prettier */
|
|
}}
|
|
</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"
|
|
data-testId="offerButton"
|
|
@click="openOfferDialog(contact.did, contact.name)"
|
|
>
|
|
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"
|
|
>
|
|
<font-awesome icon="file-lines" class="fa-fw" />
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<p v-else>There are no contacts.</p>
|
|
|
|
<div v-if="contacts.length > 0" class="mt-2 w-full text-left">
|
|
<input
|
|
v-if="!showGiveNumbers"
|
|
type="checkbox"
|
|
:checked="contactsSelected.length === contacts.length"
|
|
class="align-middle ml-2 h-6 w-6"
|
|
data-testId="contactCheckAllBottom"
|
|
@click="
|
|
contactsSelected.length === contacts.length
|
|
? (contactsSelected = [])
|
|
: (contactsSelected = contacts.map((contact) => contact.did))
|
|
"
|
|
/>
|
|
<button
|
|
v-if="!showGiveNumbers"
|
|
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 ml-2 px-1 py-1 rounded-md"
|
|
:style="
|
|
contactsSelected.length > 0
|
|
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
|
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
|
"
|
|
@click="copySelectedContacts()"
|
|
>
|
|
Copy Selections
|
|
</button>
|
|
</div>
|
|
|
|
<GiftedDialog ref="customGivenDialog" />
|
|
<OfferDialog ref="customOfferDialog" />
|
|
<ContactNameDialog ref="contactNameDialog" />
|
|
|
|
<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"
|
|
:icon-size="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>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { AxiosError } from "axios";
|
|
import { Buffer } from "buffer/";
|
|
import { IndexableType } from "dexie";
|
|
import { JWTPayload } from "did-jwt";
|
|
import * as R from "ramda";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
|
import { useClipboard } from "@vueuse/core";
|
|
|
|
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 ContactNameDialog from "../components/ContactNameDialog.vue";
|
|
import TopMessage from "../components/TopMessage.vue";
|
|
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
|
|
import {
|
|
db,
|
|
logConsoleAndDb,
|
|
retrieveSettingsForActiveAccount,
|
|
updateAccountSettings,
|
|
updateDefaultSettings,
|
|
} from "../db/index";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
|
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
|
import {
|
|
CONTACT_CSV_HEADER,
|
|
createEndorserJwtForDid,
|
|
errorStringForLog,
|
|
GiveSummaryRecord,
|
|
getHeaders,
|
|
isDid,
|
|
register,
|
|
setVisibilityUtil,
|
|
UserInfo,
|
|
VerifiableCredential,
|
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
|
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
|
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
|
} from "../libs/endorserServer";
|
|
import * as libsUtil from "../libs/util";
|
|
import { generateSaveAndActivateIdentity } from "../libs/util";
|
|
|
|
@Component({
|
|
components: {
|
|
GiftedDialog,
|
|
EntityIcon,
|
|
OfferDialog,
|
|
QuickNav,
|
|
ContactNameDialog,
|
|
TopMessage,
|
|
},
|
|
})
|
|
export default class ContactsView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
$route!: RouteLocationNormalizedLoaded;
|
|
$router!: Router;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
contacts: Array<Contact> = [];
|
|
contactInput = "";
|
|
contactEdit: Contact | null = null;
|
|
contactNewName = "";
|
|
contactsSelected: Array<string> = [];
|
|
// { "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;
|
|
showPubKeyCopy = false;
|
|
showPubKeyHashCopy = false;
|
|
showGiveNumbers = false;
|
|
showGiveTotals = true;
|
|
showGiveConfirmed = true;
|
|
showLargeIdenticon?: Contact;
|
|
|
|
AppString = AppString;
|
|
libsUtil = libsUtil;
|
|
|
|
public async created() {
|
|
await db.open();
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
this.isRegistered = !!settings.isRegistered;
|
|
|
|
// if these detect a query parameter, they can and then redirect to this URL without a query parameter
|
|
// to avoid problems when they reload or they go forward & back and it tries to reprocess
|
|
await this.processContactJwt();
|
|
await this.processInviteJwt();
|
|
|
|
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 || ""),
|
|
);
|
|
}
|
|
|
|
private async processContactJwt() {
|
|
// handle a contact sent via URL
|
|
//
|
|
// For external links, use /contact-import/:jwt with a JWT that has an array of contacts
|
|
// because that will do better error checking for things like missing data on iOS platforms.
|
|
const importedContactJwt = this.$route.query["contactJwt"] as string;
|
|
if (importedContactJwt) {
|
|
// really should fully verify contents
|
|
const { payload } = decodeEndorserJwt(importedContactJwt);
|
|
const userInfo = payload["own"] as UserInfo;
|
|
const newContact = {
|
|
did: userInfo.did || payload["iss"], // ".did" is reliable as of v 0.3.49
|
|
name: userInfo.name,
|
|
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
|
|
profileImageUrl: userInfo.profileImageUrl,
|
|
publicKeyBase64: userInfo.publicEncKey,
|
|
registered: userInfo.registered,
|
|
} as Contact;
|
|
await this.addContact(newContact);
|
|
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
|
this.$router.push({ path: "/contacts" });
|
|
}
|
|
}
|
|
|
|
private async processInviteJwt() {
|
|
// handle an invite JWT sent via URL
|
|
const importedInviteJwt = this.$route.query["inviteJwt"] as string;
|
|
if (importedInviteJwt === "") {
|
|
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Blank Invite",
|
|
text: "The invite was not included, which can happen when your iOS device cuts off the link. Try pasting the full link into a browser.",
|
|
},
|
|
7000,
|
|
);
|
|
} else if (importedInviteJwt) {
|
|
// make sure user is created
|
|
if (!this.activeDid) {
|
|
this.activeDid = await generateSaveAndActivateIdentity();
|
|
}
|
|
// send invite directly to server, with auth for this user
|
|
const headers = await getHeaders(this.activeDid);
|
|
try {
|
|
const response = await this.axios.post(
|
|
this.apiServer + "/api/v2/claim",
|
|
{ jwtEncoded: importedInviteJwt },
|
|
{ headers },
|
|
);
|
|
if (response.status != 201) {
|
|
throw { error: { response: response } };
|
|
}
|
|
await updateAccountSettings(this.activeDid, { isRegistered: true });
|
|
this.isRegistered = true;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Registered",
|
|
text: "You are now registered.",
|
|
},
|
|
3000,
|
|
);
|
|
|
|
// wait for a second before continuing so they see the registration message
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
|
|
// now add the inviter as a contact
|
|
// (similar code is in InviteOneAcceptView.vue)
|
|
const payload: JWTPayload =
|
|
decodeEndorserJwt(importedInviteJwt).payload;
|
|
const registration = payload as VerifiableCredential;
|
|
(this.$refs.contactNameDialog as ContactNameDialog).open(
|
|
"Who Invited You?",
|
|
"",
|
|
async (name) => {
|
|
await this.addContact({
|
|
did: registration.vc.credentialSubject.agent.identifier,
|
|
name: name,
|
|
registered: true,
|
|
});
|
|
// wait for a second before continuing so they see the user-added message
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
this.showOnboardingInfo();
|
|
},
|
|
async () => {
|
|
// on cancel, will still add the contact
|
|
await this.addContact({
|
|
did: registration.vc.credentialSubject.agent.identifier,
|
|
name: "(person who invited you)",
|
|
registered: true,
|
|
});
|
|
// wait for a second before continuing so they see the user-added message
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
this.showOnboardingInfo();
|
|
},
|
|
);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
const fullError = "Error redeeming invite: " + errorStringForLog(error);
|
|
logConsoleAndDb(fullError, true);
|
|
let message = "Got an error sending the invite.";
|
|
if (
|
|
error.response &&
|
|
error.response.data &&
|
|
error.response.data.error
|
|
) {
|
|
if (error.response.data.error.message) {
|
|
message = error.response.data.error.message;
|
|
} else {
|
|
message = error.response.data.error;
|
|
}
|
|
} else if (error.message) {
|
|
message = error.message;
|
|
}
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error with Invite",
|
|
text: message,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
|
this.$router.push({ path: "/contacts" });
|
|
}
|
|
}
|
|
|
|
private contactNameNonBreakingSpace(contactName?: string) {
|
|
return (contactName || AppString.NO_CONTACT_NAME).replace(/\s/g, "\u00A0");
|
|
}
|
|
|
|
private danger(message: string, title: string = "Error", timeout = 5000) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: title,
|
|
text: message,
|
|
},
|
|
timeout,
|
|
);
|
|
}
|
|
|
|
private warning(message: string, title: string = "Error", timeout = 5000) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "warning",
|
|
title: title,
|
|
text: message,
|
|
},
|
|
timeout,
|
|
);
|
|
}
|
|
|
|
private showOnboardingInfo() {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "They're Added To Your List",
|
|
text: "Would you like to go to the main page now?",
|
|
onYes: async () => {
|
|
this.$router.push({ name: "home" });
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
private filteredContacts() {
|
|
return this.showGiveNumbers
|
|
? this.contactsSelected.length === 0
|
|
? this.contacts
|
|
: this.contacts.filter((contact) =>
|
|
this.contactsSelected.includes(contact.did),
|
|
)
|
|
: this.contacts;
|
|
}
|
|
|
|
private 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.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
};
|
|
|
|
try {
|
|
const headers = await getHeaders(this.activeDid, this.$notify);
|
|
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) {
|
|
const fullError = "Error loading gives: " + errorStringForLog(error);
|
|
logConsoleAndDb(fullError, true);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Load Error",
|
|
text: "Got an error loading your gives.",
|
|
},
|
|
3000,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async onClickNewContact(): Promise<void> {
|
|
const contactInput = this.contactInput.trim();
|
|
if (!contactInput) {
|
|
this.danger(
|
|
"There was no contact info to add. Try the other green buttons.",
|
|
"No Contact",
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
|
|
const jwt = getContactJwtFromJwtUrl(contactInput);
|
|
this.$router.push({ path: "/contact-import/" + jwt });
|
|
return;
|
|
}
|
|
|
|
if (
|
|
contactInput.includes(CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI) ||
|
|
contactInput.includes(CONTACT_URL_PATH_ENDORSER_CH_OLD)
|
|
) {
|
|
const jwt = getContactJwtFromJwtUrl(contactInput);
|
|
const { payload } = decodeEndorserJwt(jwt);
|
|
const userInfo = payload["own"] as UserInfo;
|
|
const newContact = {
|
|
did: userInfo.did || payload["iss"], // "did" is reliable as of v 0.3.49
|
|
name: userInfo.name,
|
|
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
|
|
profileImageUrl: userInfo.profileImageUrl,
|
|
publicKeyBase64: userInfo.publicEncKey,
|
|
registered: userInfo.registered,
|
|
} as Contact;
|
|
await this.addContact(newContact);
|
|
return;
|
|
}
|
|
|
|
if (contactInput.startsWith(CONTACT_CSV_HEADER)) {
|
|
const lines = 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) {
|
|
const fullError =
|
|
"Error adding contacts from CSV: " + errorStringForLog(e);
|
|
logConsoleAndDb(fullError, true);
|
|
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;
|
|
}
|
|
|
|
if (contactInput.startsWith("did:")) {
|
|
let did = contactInput;
|
|
let name, publicKeyInput, nextPublicKeyHashInput;
|
|
const commaPos1 = contactInput.indexOf(",");
|
|
if (commaPos1 > -1) {
|
|
did = contactInput.substring(0, commaPos1).trim();
|
|
name = contactInput.substring(commaPos1 + 1).trim();
|
|
const commaPos2 = contactInput.indexOf(",", commaPos1 + 1);
|
|
if (commaPos2 > -1) {
|
|
name = contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
|
publicKeyInput = contactInput.substring(commaPos2 + 1).trim();
|
|
const commaPos3 = contactInput.indexOf(",", commaPos2 + 1);
|
|
if (commaPos3 > -1) {
|
|
publicKeyInput = contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
|
|
nextPublicKeyHashInput = 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);
|
|
return;
|
|
}
|
|
|
|
if (contactInput.includes("[")) {
|
|
// assume there's a JSON array of contacts in the input
|
|
const jsonContactInput = contactInput.substring(
|
|
contactInput.indexOf("["),
|
|
contactInput.lastIndexOf("]") + 1,
|
|
);
|
|
try {
|
|
const contacts = JSON.parse(jsonContactInput);
|
|
this.$router.push({
|
|
name: "contact-import",
|
|
query: { contacts: JSON.stringify(contacts) },
|
|
});
|
|
} catch (e) {
|
|
const fullError =
|
|
"Error adding contacts from array: " + errorStringForLog(e);
|
|
logConsoleAndDb(fullError, true);
|
|
this.danger("The input could not be parsed.", "Invalid Contact List");
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.danger("No contact info was found in that input.", "No Contact Info");
|
|
}
|
|
|
|
private 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);
|
|
}
|
|
|
|
private 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) {
|
|
await updateDefaultSettings({
|
|
hideRegisterPromptOnNewContact: stopAsking,
|
|
});
|
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
|
}
|
|
},
|
|
onNo: async (stopAsking?: boolean) => {
|
|
if (stopAsking) {
|
|
await updateDefaultSettings({
|
|
hideRegisterPromptOnNewContact: stopAsking,
|
|
});
|
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
|
}
|
|
},
|
|
onYes: async () => {
|
|
await this.register(newContact);
|
|
},
|
|
promptToStopAsking: true,
|
|
},
|
|
-1,
|
|
);
|
|
}, 1000);
|
|
}
|
|
}
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Contact Added",
|
|
text: addedMessage,
|
|
},
|
|
3000,
|
|
);
|
|
})
|
|
.catch((err) => {
|
|
const fullError =
|
|
"Error when adding contact to storage: " + errorStringForLog(err);
|
|
logConsoleAndDb(fullError, true);
|
|
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", 5000);
|
|
});
|
|
}
|
|
|
|
// note that this is also in DIDView.vue
|
|
private 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,
|
|
);
|
|
}
|
|
|
|
// note that this is also in DIDView.vue
|
|
private 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;
|
|
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.",
|
|
},
|
|
3000,
|
|
);
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Registration Error",
|
|
text:
|
|
(regResult.error as string) ||
|
|
"Something went wrong during registration.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
const fullError = "Error when registering: " + errorStringForLog(error);
|
|
logConsoleAndDb(fullError, true);
|
|
let userMessage = "There was an error.";
|
|
const serverError = error as AxiosError;
|
|
if (serverError.isAxiosError) {
|
|
if (
|
|
serverError.response?.data &&
|
|
typeof serverError.response.data === "object" &&
|
|
"error" in serverError.response.data &&
|
|
typeof serverError.response.data.error === "object" &&
|
|
serverError.response.data.error !== null &&
|
|
"message" in serverError.response.data.error
|
|
) {
|
|
userMessage = serverError.response.data.error.message as string;
|
|
} 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,
|
|
);
|
|
}
|
|
}
|
|
|
|
// note that this is also in DIDView.vue
|
|
private 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 {
|
|
console.error(
|
|
"Got strange result from setting visibility. It can happen when setting visibility on oneself.",
|
|
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;
|
|
}
|
|
}
|
|
|
|
private 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.push({
|
|
name: "contact-amounts",
|
|
query: { contactDid: giverDid },
|
|
});
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
} else {
|
|
this.showGiftedDialog(giverDid, recipientDid);
|
|
}
|
|
}
|
|
|
|
private showGiftedDialog(giverDid: string, recipientDid: string) {
|
|
let giver: libsUtil.GiverReceiverInputInfo | undefined;
|
|
let receiver: libsUtil.GiverReceiverInputInfo | undefined;
|
|
if (giverDid) {
|
|
giver = {
|
|
did: giverDid,
|
|
name: libsUtil.nameForDid(this.activeDid, this.contacts, giverDid),
|
|
};
|
|
}
|
|
if (recipientDid) {
|
|
receiver = {
|
|
did: recipientDid,
|
|
name: libsUtil.nameForDid(this.activeDid, 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 || "Someone Unnamed");
|
|
} 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 || "Someone Unnamed");
|
|
}
|
|
(this.$refs.customGivenDialog as GiftedDialog).open(
|
|
giver,
|
|
receiver,
|
|
undefined as unknown as string,
|
|
customTitle,
|
|
undefined as unknown as string,
|
|
callback,
|
|
);
|
|
}
|
|
|
|
openOfferDialog(recipientDid: string, recipientName?: string) {
|
|
(this.$refs.customOfferDialog as OfferDialog).open(
|
|
recipientDid,
|
|
recipientName,
|
|
);
|
|
}
|
|
|
|
private async toggleShowContactAmounts() {
|
|
const newShowValue = !this.showGiveNumbers;
|
|
try {
|
|
await updateDefaultSettings({
|
|
showContactGivesInline: newShowValue,
|
|
});
|
|
} catch (err) {
|
|
const fullError =
|
|
"Error updating contact-amounts setting: " + errorStringForLog(err);
|
|
logConsoleAndDb(fullError, true);
|
|
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.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
private 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;
|
|
}
|
|
}
|
|
|
|
private 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,
|
|
};
|
|
}
|
|
|
|
private async copySelectedContacts() {
|
|
if (this.contactsSelected.length === 0) {
|
|
this.danger("You must select contacts to copy.");
|
|
return;
|
|
}
|
|
const selectedContactsFull = this.contacts.filter((c) =>
|
|
this.contactsSelected.includes(c.did),
|
|
);
|
|
const selectedContacts: Array<Contact> = selectedContactsFull.map((c) => {
|
|
const contact: Contact = {
|
|
did: c.did,
|
|
name: c.name,
|
|
};
|
|
if (c.nextPubKeyHashB64) {
|
|
contact.nextPubKeyHashB64 = c.nextPubKeyHashB64;
|
|
}
|
|
if (c.profileImageUrl) {
|
|
contact.profileImageUrl = c.profileImageUrl;
|
|
}
|
|
if (c.publicKeyBase64) {
|
|
contact.publicKeyBase64 = c.publicKeyBase64;
|
|
}
|
|
return contact;
|
|
});
|
|
// console.log(
|
|
// "Array of selected contacts:",
|
|
// JSON.stringify(selectedContacts),
|
|
// );
|
|
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
|
contacts: selectedContacts,
|
|
});
|
|
const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt;
|
|
useClipboard()
|
|
.copy(contactsJwtUrl)
|
|
.then(() => {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "info",
|
|
title: "Copied",
|
|
text: "The link for those contacts is now in the clipboard.",
|
|
},
|
|
3000,
|
|
);
|
|
});
|
|
}
|
|
|
|
private showCopySelectionsInfo() {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "info",
|
|
title: "Copying Contacts",
|
|
text: "Contact info will include name, ID, profile image, and public key.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
private async showOnboardMeetingDialog() {
|
|
try {
|
|
// First check if they're in a meeting
|
|
const headers = await getHeaders(this.activeDid);
|
|
const memberResponse = await this.axios.get(
|
|
this.apiServer + "/api/partner/groupOnboardMember",
|
|
{ headers },
|
|
);
|
|
|
|
if (memberResponse.data.data) {
|
|
// They're in a meeting, check if they're the host
|
|
const hostResponse = await this.axios.get(
|
|
this.apiServer + "/api/partner/groupOnboard",
|
|
{ headers },
|
|
);
|
|
|
|
if (hostResponse.data.data) {
|
|
// They're the host, take them to setup
|
|
this.$router.push({ name: "onboard-meeting-setup" });
|
|
} else {
|
|
// They're not the host, take them to list
|
|
this.$router.push({ name: "onboard-meeting-list" });
|
|
}
|
|
} else {
|
|
// They're not in a meeting, show the dialog
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Onboarding Meeting",
|
|
text: "Would you like to start a new meeting?",
|
|
onYes: async () => {
|
|
this.$router.push({ name: "onboard-meeting-setup" });
|
|
},
|
|
yesText: "Start New Meeting",
|
|
onNo: async () => {
|
|
this.$router.push({ name: "onboard-meeting-list" });
|
|
},
|
|
noText: "Join Existing Meeting",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
logConsoleAndDb(
|
|
"Error checking meeting status:" + errorStringForLog(error),
|
|
);
|
|
this.danger(
|
|
"There was an error checking your meeting status.",
|
|
"Meeting Error",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
</script>
|