From c95b2178efcd9d4d4c280fbf30b0a6c89db5b799 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Tue, 20 Aug 2024 19:39:29 -0600 Subject: [PATCH] copy a list of contacts and then import --- CHANGELOG.md | 4 + src/db/tables/contacts.ts | 4 +- src/views/ContactQRScanShowView.vue | 4 +- src/views/ContactsView.vue | 256 +++++++++++++++++++++------- src/views/DIDView.vue | 5 +- 5 files changed, 202 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c54f1a0..c13a4aa38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## ? +### Added +- Send list of contacts to someone +### Changed +- Moved contact actions from list onto detail page ## [0.3.20] - 2024.08.18 - 4064eb75a9743ca268bf00016fa0a5fc5dec4e30 diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts index 4ddc4dec1..e6369b228 100644 --- a/src/db/tables/contacts.ts +++ b/src/db/tables/contacts.ts @@ -4,8 +4,8 @@ export interface Contact { nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key profileImageUrl?: string; publicKeyBase64?: string; - seesMe?: boolean; - registered?: boolean; + seesMe?: boolean; // cached value of the server setting + registered?: boolean; // cached value of the server setting } export const ContactSchema = { diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 4490211eb..aff032acb 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -458,9 +458,9 @@ export default class ContactQRScanShow extends Vue { group: "alert", type: "info", title: "Copied", - text: "Your DID was copied to the clipboard. Have them paste it on their 'People' screen to add you.", + text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.", }, - 10000, + 5000, ); }); } diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index b2714e5c6..524ddd0a6 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -41,14 +41,43 @@ -
- +
+
+ + +
+ +
+ +
@@ -86,18 +115,37 @@ >
  • -

    +
    - {{ contact.name || AppString.NO_CONTACT_NAME }} + + + +

    + {{ contact.name || AppString.NO_CONTACT_NAME }} +

    + -

    +

    There are no contacts.

    +
    + +
    + @@ -203,6 +267,7 @@ import { IndexableType } from "dexie"; import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; +import { useClipboard } from "@vueuse/core"; import { AppString, NotificationIface } from "@/constants/app"; import { db } from "@/db/index"; @@ -237,6 +302,7 @@ export default class ContactsView extends Vue { contactInput = ""; contactEdit: Contact | null = null; contactNewName = ""; + contactsSelected: Array = []; // { "did:...": concatenated-descriptions } entry for each contact givenByMeDescriptions: Record = {}; // { "did:...": amount } entry for each contact @@ -262,7 +328,7 @@ export default class ContactsView extends Vue { AppString = AppString; libsUtil = libsUtil; - async created() { + public async created() { await db.open(); const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; this.activeDid = settings?.activeDid || ""; @@ -285,7 +351,7 @@ export default class ContactsView extends Vue { ); } - danger(message: string, title: string = "Error", timeout = 5000) { + private danger(message: string, title: string = "Error", timeout = 5000) { this.$notify( { group: "alert", @@ -297,7 +363,17 @@ export default class ContactsView extends Vue { ); } - async loadGives() { + 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; } @@ -404,19 +480,20 @@ export default class ContactsView extends Vue { } } - async onClickNewContact(): Promise { - if (!this.contactInput) { + private async onClickNewContact(): Promise { + const contactInput = this.contactInput.trim(); + if (!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); + if (contactInput.startsWith(CONTACT_URL_PREFIX)) { + await this.addContactFromScan(contactInput); return; } - if (this.contactInput.startsWith(CONTACT_CSV_HEADER)) { - const lines = this.contactInput.split(/\n/); + 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)) { @@ -448,44 +525,71 @@ export default class ContactsView extends Vue { 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 + 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; } - // 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 + + 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 as Router).push({ + name: "contact-import", + query: { contacts: JSON.stringify(contacts) }, + }); + } catch (e) { + this.danger("The input could not be parsed.", "Invalid Contact List"); + } + return; } - const newContact = { - did, - name, - publicKeyBase64, - nextPubKeyHashB64: nextPubKeyHashB64, - }; - await this.addContact(newContact); + + this.danger("No contact info was found in that input.", "No Contact Info"); } - async addContactFromEndorserMobileLine(line: string): Promise { + private async addContactFromEndorserMobileLine( + line: string, + ): Promise { // Note that Endorser Mobile puts name first, then did, etc. let name = line; let did = ""; @@ -526,7 +630,7 @@ export default class ContactsView extends Vue { return db.contacts.add(newContact); } - async addContactFromScan(url: string): Promise { + private async addContactFromScan(url: string): Promise { const payload = getContactPayloadFromJwtUrl(url); if (!payload) { this.$notify( @@ -551,7 +655,7 @@ export default class ContactsView extends Vue { } } - async addContact(newContact: Contact) { + private async addContact(newContact: Contact) { if (!newContact.did) { this.danger("Cannot add a contact without a DID.", "Incomplete Contact"); return; @@ -641,7 +745,7 @@ export default class ContactsView extends Vue { } // note that this is also in DIDView.vue - async confirmSetVisibility(contact: Contact, visibility: boolean) { + 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?"; @@ -663,7 +767,7 @@ export default class ContactsView extends Vue { } // note that this is also in DIDView.vue - async register(contact: Contact) { + private async register(contact: Contact) { this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000); try { @@ -729,7 +833,7 @@ export default class ContactsView extends Vue { } // note that this is also in DIDView.vue - async setVisibility( + private async setVisibility( contact: Contact, visibility: boolean, showSuccessAlert: boolean, @@ -779,7 +883,7 @@ export default class ContactsView extends Vue { } // note that this is also in DIDView.vue - async checkVisibility(contact: Contact) { + private async checkVisibility(contact: Contact) { const url = this.apiServer + "/api/report/canDidExplicitlySeeMe?did=" + @@ -846,7 +950,7 @@ export default class ContactsView extends Vue { } } - confirmShowGiftedDialog(giverDid: string, recipientDid: string) { + private confirmShowGiftedDialog(giverDid: string, recipientDid: string) { // if they have unconfirmed amounts, ask to confirm those if ( recipientDid === this.activeDid && @@ -936,7 +1040,7 @@ export default class ContactsView extends Vue { ); } - public async toggleShowContactAmounts() { + private async toggleShowContactAmounts() { const newShowValue = !this.showGiveNumbers; try { await db.open(); @@ -972,7 +1076,7 @@ export default class ContactsView extends Vue { this.loadGives(); } } - public toggleShowGiveTotals() { + private toggleShowGiveTotals() { if (this.showGiveTotals) { this.showGiveTotals = false; this.showGiveConfirmed = true; @@ -985,7 +1089,7 @@ export default class ContactsView extends Vue { } } - public showGiveAmountsClassNames() { + private showGiveAmountsClassNames() { return { "from-slate-400": this.showGiveTotals, "to-slate-700": this.showGiveTotals, @@ -995,5 +1099,31 @@ export default class ContactsView extends Vue { "to-yellow-700": !this.showGiveTotals && !this.showGiveConfirmed, }; } + + private copySelectedContacts() { + if (this.contactsSelected.length === 0) { + this.danger("You must select contacts to copy."); + return; + } + const selectedContacts = this.contacts.filter((c) => + this.contactsSelected.includes(c.did), + ); + const message = + "To add contacts, paste this into the box on the 'People' screen.\n\n" + + JSON.stringify(selectedContacts, null, 2); + useClipboard() + .copy(message) + .then(() => { + this.$notify( + { + group: "alert", + type: "info", + title: "Copied", + text: "Those contacts were copied to the clipboard. Have them paste it in the box on their 'People' screen.", + }, + 5000, + ); + }); + } } diff --git a/src/views/DIDView.vue b/src/views/DIDView.vue index a2dd9213a..583b3b13e 100644 --- a/src/views/DIDView.vue +++ b/src/views/DIDView.vue @@ -22,10 +22,7 @@

    - {{ - didInfoForContact(viewingDid, activeDid, contact, allMyDids) - .displayName - }} + {{ contact?.name || "(no name)" }}