|
|
@ -16,96 +16,135 @@ |
|
|
|
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 v-if="checkingImports" class="text-center"> |
|
|
|
<fa 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> |
|
|
|
|
|
|
|
<!-- 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" |
|
|
|
<div v-if="sameCount > 0"> |
|
|
|
<span v-if="sameCount == 1" |
|
|
|
>One contact is the same as an existing contact</span |
|
|
|
> |
|
|
|
<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" |
|
|
|
<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 |
|
|
|
> |
|
|
|
<div class="border p-1">{{ contactField }}</div> |
|
|
|
<div class="border p-1">{{ value.old }}</div> |
|
|
|
<div class="border p-1">{{ value.new }}</div> |
|
|
|
<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> |
|
|
|
</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> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script lang="ts"> |
|
|
|
import { JWTVerified } from "did-jwt"; |
|
|
|
import { JWTPayload, JWTVerified } from "did-jwt"; |
|
|
|
import * as R from "ramda"; |
|
|
|
import { Component, Vue } from "vue-facing-decorator"; |
|
|
|
import { Router } from "vue-router"; |
|
|
|
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 { AppString, NotificationIface } from "@/constants/app"; |
|
|
|
import { db, retrieveSettingsForActiveAccount } from "@/db/index"; |
|
|
|
import { Contact } from "@/db/tables/contacts"; |
|
|
|
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 { decodeAndVerifyJwt } from "@/libs/crypto/vc/index"; |
|
|
|
import { setVisibilityUtil } from "@/libs/endorserServer"; |
|
|
|
import { decodeAndVerifyJwt } from "@/libs/crypto/vc"; |
|
|
|
import { |
|
|
|
capitalizeAndInsertSpacesBeforeCaps, |
|
|
|
errorStringForLog, |
|
|
|
setVisibilityUtil, |
|
|
|
} from "@/libs/endorserServer"; |
|
|
|
import { getContactPayloadFromJwtUrl } from "@/libs/crypto"; |
|
|
|
|
|
|
|
@Component({ |
|
|
|
components: { EntityIcon, OfferDialog, QuickNav }, |
|
|
@ -114,6 +153,7 @@ export default class ContactImportView extends Vue { |
|
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void; |
|
|
|
|
|
|
|
AppString = AppString; |
|
|
|
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps; |
|
|
|
libsUtil = libsUtil; |
|
|
|
R = R; |
|
|
|
|
|
|
@ -126,10 +166,14 @@ export default class ContactImportView extends Vue { |
|
|
|
string, |
|
|
|
Record< |
|
|
|
string, |
|
|
|
{ new: string | boolean | undefined; old: string | boolean | undefined } |
|
|
|
{ |
|
|
|
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 |
|
|
|
importing = false; |
|
|
|
checkingImports = false; |
|
|
|
inputJwt: string = ""; |
|
|
|
makeVisible = true; |
|
|
|
sameCount = 0; |
|
|
|
|
|
|
@ -139,9 +183,8 @@ export default class ContactImportView extends Vue { |
|
|
|
this.apiServer = settings.apiServer || ""; |
|
|
|
|
|
|
|
// look for any imported contacts from the query parameter |
|
|
|
const importedContacts = (this.$route as Router).query[ |
|
|
|
"contacts" |
|
|
|
] as string; |
|
|
|
const importedContacts = (this.$route as RouteLocationNormalizedLoaded) |
|
|
|
.query["contacts"] as string; |
|
|
|
if (importedContacts) { |
|
|
|
await this.setContactsSelected(JSON.parse(importedContacts)); |
|
|
|
} |
|
|
@ -176,13 +219,13 @@ export default class ContactImportView extends Vue { |
|
|
|
const differences: Record< |
|
|
|
string, |
|
|
|
{ |
|
|
|
new: string | boolean | undefined; |
|
|
|
old: string | boolean | undefined; |
|
|
|
new: string | boolean | Array<ContactMethod> | undefined; |
|
|
|
old: string | boolean | Array<ContactMethod> | undefined; |
|
|
|
} |
|
|
|
> = {}; |
|
|
|
Object.keys(contactIn).forEach((key) => { |
|
|
|
// eslint-disable-next-line prettier/prettier |
|
|
|
if (contactIn[key as keyof Contact] !== existingContact[key as keyof Contact]) { |
|
|
|
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], |
|
|
@ -200,8 +243,56 @@ export default class ContactImportView extends Vue { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 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 payload: JWTPayload = getContactPayloadFromJwtUrl(jwtInput); |
|
|
|
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.importing = true; |
|
|
|
this.checkingImports = true; |
|
|
|
let importedCount = 0, |
|
|
|
updatedCount = 0; |
|
|
|
for (let i = 0; i < this.contactsImporting.length; i++) { |
|
|
@ -253,7 +344,7 @@ export default class ContactImportView extends Vue { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.importing = false; |
|
|
|
this.checkingImports = false; |
|
|
|
|
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|