|
|
|
|
@@ -41,14 +41,43 @@
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="w-full text-right">
|
|
|
|
|
<button
|
|
|
|
|
href=""
|
|
|
|
|
class="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 py-1 rounded-md"
|
|
|
|
|
@click="toggleShowContactAmounts()"
|
|
|
|
|
>
|
|
|
|
|
{{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
|
|
|
|
|
</button>
|
|
|
|
|
<div class="flex justify-between">
|
|
|
|
|
<div class="w-full text-left">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
v-if="!showGiveNumbers"
|
|
|
|
|
:checked="contactsSelected.length === contacts.length"
|
|
|
|
|
@click="
|
|
|
|
|
contactsSelected.length === contacts.length
|
|
|
|
|
? (contactsSelected = [])
|
|
|
|
|
: (contactsSelected = contacts.map((contact) => contact.did))
|
|
|
|
|
"
|
|
|
|
|
class="align-middle ml-2 h-6 w-6"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
href=""
|
|
|
|
|
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 ml-2 rounded-md"
|
|
|
|
|
:style="
|
|
|
|
|
contactsSelected.length > 0
|
|
|
|
|
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
|
|
|
|
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
|
|
|
|
"
|
|
|
|
|
@click="copySelectedContacts()"
|
|
|
|
|
v-if="!showGiveNumbers"
|
|
|
|
|
>
|
|
|
|
|
Copy Selected Contacts
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="w-full text-right">
|
|
|
|
|
<button
|
|
|
|
|
href=""
|
|
|
|
|
class="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 py-1 rounded-md"
|
|
|
|
|
@click="toggleShowContactAmounts()"
|
|
|
|
|
>
|
|
|
|
|
{{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex justify-between mt-1" v-if="showGiveNumbers">
|
|
|
|
|
<div class="w-full text-right">
|
|
|
|
|
@@ -86,18 +115,37 @@
|
|
|
|
|
>
|
|
|
|
|
<li
|
|
|
|
|
class="border-b border-slate-300 pt-1 pb-1"
|
|
|
|
|
v-for="contact in contacts"
|
|
|
|
|
v-for="contact in filteredContacts()"
|
|
|
|
|
:key="contact.did"
|
|
|
|
|
>
|
|
|
|
|
<div class="grow overflow-hidden">
|
|
|
|
|
<h2 class="text-base font-semibold">
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
<EntityIcon
|
|
|
|
|
:contact="contact"
|
|
|
|
|
:iconSize="24"
|
|
|
|
|
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
|
|
|
|
|
@click="showLargeIdenticon = contact"
|
|
|
|
|
/>
|
|
|
|
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
v-if="!showGiveNumbers"
|
|
|
|
|
:checked="contactsSelected.includes(contact.did)"
|
|
|
|
|
@click="
|
|
|
|
|
contactsSelected.includes(contact.did)
|
|
|
|
|
? contactsSelected.splice(
|
|
|
|
|
contactsSelected.indexOf(contact.did),
|
|
|
|
|
1,
|
|
|
|
|
)
|
|
|
|
|
: contactsSelected.push(contact.did)
|
|
|
|
|
"
|
|
|
|
|
class="ml-2 h-6 w-6"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<h2 class="text-base font-semibold ml-2">
|
|
|
|
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
<router-link
|
|
|
|
|
:to="{
|
|
|
|
|
path: '/did/' + encodeURIComponent(contact.did),
|
|
|
|
|
@@ -106,7 +154,7 @@
|
|
|
|
|
>
|
|
|
|
|
<fa icon="circle-info" class="text-blue-500 ml-4" />
|
|
|
|
|
</router-link>
|
|
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
|
|
|
|
<div
|
|
|
|
|
v-if="showGiveNumbers && contact.did != activeDid"
|
|
|
|
|
@@ -178,6 +226,22 @@
|
|
|
|
|
</ul>
|
|
|
|
|
<p v-else>There are no contacts.</p>
|
|
|
|
|
|
|
|
|
|
<div class="w-full text-left">
|
|
|
|
|
<button
|
|
|
|
|
href=""
|
|
|
|
|
class="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 py-1 rounded-md"
|
|
|
|
|
:style="
|
|
|
|
|
contactsSelected.length > 0
|
|
|
|
|
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
|
|
|
|
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
|
|
|
|
"
|
|
|
|
|
@click="copySelectedContacts()"
|
|
|
|
|
v-if="!showGiveNumbers"
|
|
|
|
|
>
|
|
|
|
|
Copy Selected Contacts
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<GiftedDialog ref="customGivenDialog" />
|
|
|
|
|
<OfferDialog ref="customOfferDialog" />
|
|
|
|
|
|
|
|
|
|
@@ -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<string> = [];
|
|
|
|
|
// { "did:...": concatenated-descriptions } entry for each contact
|
|
|
|
|
givenByMeDescriptions: Record<string, string> = {};
|
|
|
|
|
// { "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<void> {
|
|
|
|
|
if (!this.contactInput) {
|
|
|
|
|
private async onClickNewContact(): Promise<void> {
|
|
|
|
|
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");
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
this.danger("No contact info was found in that input.", "No Contact Info");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async addContactFromEndorserMobileLine(line: string): Promise<IndexableType> {
|
|
|
|
|
private async addContactFromEndorserMobileLine(
|
|
|
|
|
line: string,
|
|
|
|
|
): Promise<IndexableType> {
|
|
|
|
|
// 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<void> {
|
|
|
|
|
private async addContactFromScan(url: string): Promise<void> {
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|