timesafari
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

392 lines
13 KiB

<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()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Contact Import
</h1>
<div v-if="checkingImports" class="text-center">
<font-awesome icon="spinner" class="animate-spin" />
</div>
<div v-else>
<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></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 font-bold p-1">
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
</div>
<div class="border p-1">{{ value.old }}</div>
<div class="border p-1">{{ value.new }}</div>
</div>
</div>
</div>
</div>
</li>
<button
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-if="contactsImporting.length > 0">
All those contacts are already in your list with the same information.
</p>
<div v-else>
There are no contacts in that import. If some were sent, try again to
get the full text and paste it. (Note that iOS cuts off data in text
messages.) Ask the person to send the data a different way, eg. email.
<div class="mt-4 text-center">
<textarea
v-model="inputJwt"
placeholder="Contact-import data"
class="mt-4 border-2 border-gray-300 p-2 rounded"
cols="30"
@input="() => checkContactJwt(inputJwt)"
/>
<br />
<button
@click="() => processContactJwt(inputJwt)"
class="ml-2 p-2 bg-blue-500 text-white rounded"
>
Check Import
</button>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue";
import OfferDialog from "../components/OfferDialog.vue";
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
import {
db,
logConsoleAndDb,
retrieveSettingsForActiveAccount,
} from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
import * as libsUtil from "../libs/util";
import {
capitalizeAndInsertSpacesBeforeCaps,
errorStringForLog,
setVisibilityUtil,
} from "../libs/endorserServer";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";
@Component({
components: { EntityIcon, OfferDialog, QuickNav },
})
export default class ContactImportView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
AppString = AppString;
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
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 | boolean | Array<ContactMethod> | undefined;
old: string | boolean | Array<ContactMethod> | undefined;
}
>
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
checkingImports = false;
inputJwt: string = "";
makeVisible = true;
sameCount = 0;
async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
// look for any imported contact array from the query parameter
const importedContacts = (this.$route as RouteLocationNormalizedLoaded)
.query["contacts"] as string;
if (importedContacts) {
await this.setContactsSelected(JSON.parse(importedContacts));
}
// look for a JWT after /contact-import/ in the window.location.pathname
const jwt = window.location.pathname.match(
/\/contact-import\/(ey.+)$/,
)?.[1];
if (jwt) {
// would prefer to validate but we've got an error with JWTs on QR codes generated in the future
// eslint-disable-next-line prettier/prettier
// const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt);
// decode the JWT
const parsedJwt = decodeEndorserJwt(jwt);
const contacts: Array<Contact> =
parsedJwt.payload.contacts || // someday this will be the only payload sent to this page
(Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);
if (!contacts && parsedJwt.payload.own) {
// handle this single-contact JWT in the contacts page, better suited to single additions
(this.$router as Router).push({
name: "contacts",
query: { contactJwt: jwt },
});
}
if (contacts) {
await this.setContactsSelected(contacts);
} else {
// no contacts found so default message should be OK
}
}
if (
this.contactsImporting.length === 1 &&
R.isEmpty(this.contactsExisting)
) {
// if there is only one contact and it's new, then we will automatically import it
this.contactsSelected[0] = true;
this.importContacts(); // ... which routes to the contacts list
}
}
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 | boolean | Array<ContactMethod> | undefined;
old: string | boolean | Array<ContactMethod> | undefined;
}
> = {};
Object.keys(contactIn).forEach((key) => {
// eslint-disable-next-line prettier/prettier
if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) {
differences[key] = {
old: existingContact[key as keyof Contact],
new: contactIn[key as keyof Contact],
};
}
});
this.contactDifferences[contactIn.did] = differences;
if (R.isEmpty(differences)) {
this.sameCount++;
}
// don't automatically import previous data
this.contactsSelected[i] = false;
}
}
}
// check the contact-import JWT
async checkContactJwt(jwtInput: string) {
if (
jwtInput.endsWith(APP_SERVER) ||
jwtInput.endsWith(APP_SERVER + "/") ||
jwtInput.endsWith("contact-import") ||
jwtInput.endsWith("contact-import/")
) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "That is only part of the contact-import data; it's missing data at the end. Try another way to get the full data.",
},
5000,
);
}
}
// process the invite JWT and/or text message containing the URL with the JWT
async processContactJwt(jwtInput: string) {
this.checkingImports = true;
try {
// (For another approach used with invites, see InviteOneAcceptView.processInvite)
const jwt: string = getContactJwtFromJwtUrl(jwtInput);
// JWT format: { header, payload, signature, data }
const payload = decodeEndorserJwt(jwt).payload;
if (Array.isArray(payload.contacts)) {
await this.setContactsSelected(payload.contacts);
} else {
throw new Error("Invalid contact-import JWT or URL: " + jwtInput);
}
} catch (error) {
const fullError = "Error importing contacts: " + errorStringForLog(error);
logConsoleAndDb(fullError, true);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error processing the contact-import data.",
},
3000,
);
}
this.checkingImports = false;
}
async importContacts() {
this.checkingImports = 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.
// DataError: Failed to execute 'add' on 'IDBObjectStore': Evaluating the object store's key path yielded a value that is not a valid key.
await db.contacts.add(R.clone(contact));
importedCount++;
}
}
}
if (this.makeVisible) {
const failedVisibileToContacts = [];
for (let i = 0; i < this.contactsImporting.length; i++) {
if (this.contactsSelected[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 > 0) {
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.checkingImports = 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>