Browse Source

import & update selected contacts

Trent Larson 4 months ago
parent
commit
5647c4627f
  1. 5
      src/router/index.ts
  2. 67
      src/views/AccountViewView.vue
  3. 179
      src/views/ContactImportView.vue

5
src/router/index.ts

@ -63,6 +63,11 @@ const routes: Array<RouteRecordRaw> = [
name: "contact-gift", name: "contact-gift",
component: () => import("../views/ContactGiftingView.vue"), component: () => import("../views/ContactGiftingView.vue"),
}, },
{
path: "/contact-import",
name: "contact-import",
component: () => import("../views/ContactImportView.vue"),
},
{ {
path: "/contact-qr", path: "/contact-qr",
name: "contact-qr", name: "contact-qr",

67
src/views/AccountViewView.vue

@ -216,7 +216,7 @@
<div class="mb-2 font-bold">Location</div> <div class="mb-2 font-bold">Location</div>
<router-link <router-link
:to="{ name: 'search-area' }" :to="{ name: 'search-area' }"
class="block w-full text-center text-m 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-2 rounded-md mb-2 mt-6" class="block w-full text-center text-m 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-2 rounded-md mb-2 mt-6"
> >
Set Search Area Set Search Area
<!-- If already set, change button label to "Change Search Area" --> <!-- If already set, change button label to "Change Search Area" -->
@ -282,14 +282,14 @@
<router-link <router-link
:to="{ name: 'seed-backup' }" :to="{ name: 'seed-backup' }"
v-if="activeDid" v-if="activeDid"
class="block w-full text-center 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.5 py-2 rounded-md mb-2 mt-2" class="block w-full text-center text-md 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-2 rounded-md mb-2 mt-2"
> >
Backup Identifier Seed Backup Identifier Seed
</router-link> </router-link>
<button <button
v-bind:class="computedStartDownloadLinkClassNames()" v-bind:class="computedStartDownloadLinkClassNames()"
class="block w-full text-center 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.5 py-2 rounded-md" class="block w-full text-center text-md 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-2 rounded-md"
@click="exportDatabase()" @click="exportDatabase()"
> >
Download Settings & Contacts Download Settings & Contacts
@ -303,19 +303,20 @@
> >
If no download happened yet, click again here to download now. If no download happened yet, click again here to download now.
</a> </a>
<div v-if="downloadUrl"> <div>
<p> <p>
After the download, you can save the file in your preferred storage After the download, you can save the file in your preferred storage
location. location.
</p> </p>
<ul> <ul>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
On iOS: Choose "More..." and select anyplace in iCloud, or go "Back" On iOS: Choose "More..." and select a place in iCloud, or go "Back"
and save to another location. and save to another location.
</li> </li>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
On Android: Choose "Open" and then share to your prefered place. On Android: Choose "Open" and then share
<fa icon="share-nodes" class="fa-fw" /> <fa icon="share-nodes" class="fa-fw" />
to your prefered place.
</li> </li>
</ul> </ul>
</div> </div>
@ -410,20 +411,27 @@
<div class="mt-4"> <div class="mt-4">
<h2 class="text-slate-500 text-sm font-bold"> <h2 class="text-slate-500 text-sm font-bold">
Contacts & Settings Database Import Contacts & Settings Database
</h2> </h2>
<div class="ml-4 mt-2"> <div class="ml-4 mt-2">
Import
<input type="file" @change="uploadImportFile" class="ml-2" /> <input type="file" @change="uploadImportFile" class="ml-2" />
<div v-if="showContactImport()"> <div v-if="showContactImport()" class="mt-4">
<button <button
class="block text-center text-md 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-2 rounded-md mb-6" class="block text-center text-md 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-2 rounded-md mb-6"
@click="confirmSubmitImportFile()" @click="confirmSubmitImportFile()"
> >
Import Settings & Contacts Overwrite Settings & Contacts
<br /> <br />
(excluding Identifier Data) (which doesn't include Identifier Data)
</button>
<button
class="block text-center text-md 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-2 rounded-md mb-6"
@click="checkContactImports()"
>
Import Contacts
<br />
after comparing
</button> </button>
</div> </div>
</div> </div>
@ -692,9 +700,11 @@ import { Buffer } from "buffer/";
import Dexie from "dexie"; import Dexie from "dexie";
import "dexie-export-import"; import "dexie-export-import";
import { ImportProgress } from "dexie-export-import/dist/import"; import { ImportProgress } from "dexie-export-import/dist/import";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { ref } from "vue"; import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
@ -710,6 +720,7 @@ import {
} from "@/constants/app"; } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES, DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY, MASTER_SETTINGS_KEY,
@ -1146,6 +1157,40 @@ export default class AccountViewView extends Vue {
} }
} }
async checkContactImports() {
const reader = new FileReader();
reader.onload = (event) => {
const fileContent: string = (event.target?.result as string) || "{}";
try {
const contents = JSON.parse(fileContent);
const contactTableRows: Array<Contact> = (
contents.data?.data as [{ tableName: string; rows: Array<Contact> }]
)?.find((table) => table.tableName === "contacts")
?.rows as Array<Contact>;
const contactRows = contactTableRows.map(
// @ts-expect-error for omitting this field that is found in the Dexie format
(contact) => R.omit(["$types"], contact) as Contact,
);
(this.$router as Router).push({
name: "contact-import",
query: { contacts: JSON.stringify(contactRows) },
});
} catch (error) {
console.error("Error checking contact imports:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Importing",
text: "There was an error reading that Dexie file.",
},
3000,
);
}
};
reader.readAsText(inputImportFileNameRef.value as Blob);
}
private progressCallback(progress: ImportProgress) { private progressCallback(progress: ImportProgress) {
console.log( console.log(
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`, `Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,

179
src/views/ContactImportView.vue

@ -0,0 +1,179 @@
<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">
Contact Import
</h1>
<div v-if="sameCount > 0">
{{ sameCount }} contact{{ sameCount == 1 ? "" : "s" }} are the same as
existing contacts.
</div>
<!-- Results List -->
<ul v-if="contactsImporting.length > 0" 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="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="importContacts"
>
Import Selected Contacts
</button>
</ul>
<p v-else>There are no contacts to import.</p>
</section>
</template>
<script lang="ts">
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
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";
@Component({
components: { GiftedDialog, EntityIcon, OfferDialog, QuickNav },
})
export default class ContactImportView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
AppString = AppString;
libsUtil = libsUtil;
R = R;
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;
sameCount = 0;
async created() {
// Retrieve the imported contacts from the query parameter
const importedContacts =
((this.$route as Router).query["contacts"] as string) || "[]";
this.contactsImporting = JSON.parse(importedContacts);
this.contactsSelected = new Array(this.contactsImporting.length).fill(
false,
);
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++;
}
} else {
// automatically import new data
this.contactsSelected[i] = true;
}
}
}
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 = R.clone(this.contactsImporting[i]); // cloning to avoid the problem with a Proxy object
const existingContact = this.contactsExisting[contact.did];
if (existingContact) {
await db.contacts.update(contact.did, contact);
updatedCount++;
} else {
await db.contacts.add(contact);
importedCount++;
}
}
}
this.importing = false;
this.$notify(
{
group: "alert",
type: "success",
title: "Import Success",
text:
`${importedCount} contact${importedCount == 1 ? "" : "s"} imported.` +
(updatedCount ? ` ${updatedCount} updated.` : ""),
},
3000,
);
(this.$router as Router).push({ name: "contacts" });
}
}
</script>
Loading…
Cancel
Save