<template> <QuickNav selected="Contacts"></QuickNav> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <!-- Back --> <div class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" @click="$router.back()" > <fa icon="chevron-left" class="fa-fw"></fa> </h1> </div> <!-- Heading --> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> Contact Import </h1> <span v-if="contactsImporting.length > sameCount" class="flex justify-center" > <input type="checkbox" v-model="makeVisible" class="mr-2" /> Make my activity visible to these contacts. </span> <div v-if="sameCount > 0"> <span v-if="sameCount == 1" >One contact is the same as an existing contact</span > <span v-else >{{ sameCount }} contacts are the same as existing contacts</span > </div> <!-- Results List --> <ul v-if="contactsImporting.length > sameCount" class="border-t border-slate-300" > <li v-for="(contact, index) in contactsImporting" :key="contact.did"> <div v-if=" !contactsExisting[contact.did] || !R.isEmpty(contactDifferences[contact.did]) " class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4" > <h2 class="text-base font-semibold"> <input type="checkbox" v-model="contactsSelected[index]" /> {{ contact.name || AppString.NO_CONTACT_NAME }} - <span v-if="contactsExisting[contact.did]" class="text-orange-500" >Existing</span > <span v-else class="text-green-500">New</span> </h2> <div class="text-sm truncate"> {{ contact.did }} </div> <div v-if="contactDifferences[contact.did]"> <div> <div class="grid grid-cols-3 gap-2"> <div class="font-bold">Field</div> <div class="font-bold">Old Value</div> <div class="font-bold">New Value</div> </div> <div v-for="(value, contactField) in contactDifferences[contact.did]" :key="contactField" class="grid grid-cols-3 border" > <div class="border p-1">{{ contactField }}</div> <div class="border p-1">{{ value.old }}</div> <div class="border p-1">{{ value.new }}</div> </div> </div> </div> </div> </li> <fa icon="spinner" v-if="importing" class="animate-spin" /> <button v-else class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-sm text-white mt-2 px-2 py-1.5 rounded" @click="importContacts" > Import Selected Contacts </button> </ul> <p v-else>There are no contacts to import.</p> </section> </template> <script lang="ts"> import { JWTVerified } from "did-jwt"; import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; import QuickNav from "../components/QuickNav.vue"; import EntityIcon from "../components/EntityIcon.vue"; import OfferDialog from "../components/OfferDialog.vue"; import { AppString, NotificationIface } from "../constants/app"; import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { Contact } from "../db/tables/contacts"; import * as libsUtil from "../libs/util"; import { decodeAndVerifyJwt } from "../libs/crypto/vc/index"; import { setVisibilityUtil } from "../libs/endorserServer"; @Component({ components: { EntityIcon, OfferDialog, QuickNav }, }) export default class ContactImportView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; AppString = AppString; libsUtil = libsUtil; R = R; activeDid = ""; apiServer = ""; contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID contactsImporting: Array<Contact> = []; // contacts from the import contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected contactDifferences: Record< string, Record<string, { new: string; old: string }> > = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key importing = false; makeVisible = true; sameCount = 0; async created() { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; this.apiServer = settings.apiServer || ""; // Retrieve the imported contacts from the query parameter const importedContacts = (this.$route as Router).query[ "contacts" ] as string; if (importedContacts) { await this.setContactsSelected(JSON.parse(importedContacts)); } // match everything after /contact-import/ in the window.location.pathname const jwt = window.location.pathname.match( /\/contact-import\/(ey.+)$/, )?.[1]; if (jwt) { // decode the JWT // eslint-disable-next-line prettier/prettier const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt); await this.setContactsSelected(parsedJwt.payload.contacts as Contact[]); } } async setContactsSelected(contacts: Array<Contact>) { this.contactsImporting = contacts; this.contactsSelected = new Array(this.contactsImporting.length).fill(true); await db.open(); const baseContacts = await db.contacts.toArray(); // set the existing contacts, keyed by DID, if they exist in contactsImporting for (let i = 0; i < this.contactsImporting.length; i++) { const contactIn = this.contactsImporting[i]; const existingContact = baseContacts.find( (contact) => contact.did === contactIn.did, ); if (existingContact) { this.contactsExisting[contactIn.did] = existingContact; const differences: Record<string, { new: string; old: string }> = {}; Object.keys(contactIn).forEach((key) => { if (contactIn[key] !== existingContact[key]) { differences[key] = { old: existingContact[key], new: contactIn[key], }; } }); this.contactDifferences[contactIn.did] = differences; if (R.isEmpty(differences)) { this.sameCount++; } // don't automatically import previous data this.contactsSelected[i] = false; } } } async importContacts() { this.importing = true; let importedCount = 0, updatedCount = 0; for (let i = 0; i < this.contactsImporting.length; i++) { if (this.contactsSelected[i]) { const contact = this.contactsImporting[i]; const existingContact = this.contactsExisting[contact.did]; if (existingContact) { await db.contacts.update(contact.did, contact); updatedCount++; } else { // without explicit clone on the Proxy, we get: DataCloneError: Failed to execute 'add' on 'IDBObjectStore': #<Object> could not be cloned. await db.contacts.add(R.clone(contact)); importedCount++; } } } if (this.makeVisible) { const failedVisibileToContacts = []; for (let i = 0; i < this.contactsImporting.length; i++) { const contact = this.contactsImporting[i]; if (contact) { const visResult = await setVisibilityUtil( this.activeDid, this.apiServer, this.axios, db, contact, true, ); if (!visResult.success) { failedVisibileToContacts.push(contact); } } } if (failedVisibileToContacts.length) { this.$notify( { group: "alert", type: "danger", title: "Visibility Error", text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${ failedVisibileToContacts.length == 1 ? "" : "s" }. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`, }, -1, ); } } this.importing = false; this.$notify( { group: "alert", type: "success", title: "Imported", text: `${importedCount} contact${importedCount == 1 ? "" : "s"} imported.` + (updatedCount ? ` ${updatedCount} updated.` : ""), }, 3000, ); (this.$router as Router).push({ name: "contacts" }); } } </script>