Merge fixes

This commit is contained in:
Matthew Raymer
2025-01-11 12:45:43 +00:00
48 changed files with 1367 additions and 822 deletions

View File

@@ -69,32 +69,36 @@
<div class="flex justify-between" v-if="contacts.length > 0">
<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"
data-testId="contactCheckAllTop"
/>
<button
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 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"
data-testId="copySelectedContactsButtonTop"
>
Copy Selections
</button>
<div v-if="!showGiveNumbers">
<input
type="checkbox"
: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"
data-testId="contactCheckAllTop"
/>
<button
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 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"
data-testId="copySelectedContactsButtonTop"
>
Copy Selections
</button>
<button @click="showCopySelectionsInfo()">
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
</button>
</div>
</div>
<div class="w-full text-right">
@@ -170,27 +174,35 @@
)
: contactsSelected.push(contact.did)
"
class="ml-2 h-6 w-6"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
/>
<h2 class="text-base font-semibold ml-2">
{{ contact.name || AppString.NO_CONTACT_NAME }}
<h2
class="text-base font-semibold ml-2 w-1/3 truncate flex-shrink-0"
>
{{ contactNameNonBreakingSpace(contact.name) }}
</h2>
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
</router-link>
<span>
<div class="flex items-center">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
</router-link>
<span class="ml-4 text-sm overflow-hidden">{{
shortDid(contact.did)
}}</span
><!-- The first 18 characters of did:peer are the same. -->
<span class="ml-4 text-sm overflow-hidden">{{
shortDid(contact.did)
}}</span>
</div>
<div class="ml-4 text-sm">
{{ contact.notes }}
</div>
</span>
</div>
<div id="ContactActions" class="flex gap-1.5 mt-2">
<div
@@ -332,11 +344,10 @@ import {
updateDefaultSettings,
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import { getContactPayloadFromJwtUrl } from "../libs/crypto";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";
import {
CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX,
createEndorserJwtForDid,
errorStringForLog,
GiveSummaryRecord,
@@ -346,6 +357,9 @@ import {
setVisibilityUtil,
UserInfo,
VerifiableCredential,
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
CONTACT_URL_PATH_ENDORSER_CH_OLD,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { generateSaveAndActivateIdentity } from "../libs/util";
@@ -402,6 +416,11 @@ export default class ContactsView extends Vue {
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;
// if these detect a query parameter, they can and then redirect to this URL without a query parameter
// to avoid problems when they reload or they go forward & back and it tries to reprocess
await this.processContactJwt();
await this.processInviteJwt();
this.showGiveNumbers = !!settings.showContactGivesInline;
this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact;
@@ -416,9 +435,13 @@ export default class ContactsView extends Vue {
this.contacts = baseContacts.sort((a, b) =>
(a.name || "").localeCompare(b.name || ""),
);
}
private async processContactJwt() {
// handle a contact sent via URL
// @deprecated: use /contact-import/:jwt with a JWT that has an array of contacts
//
// For external links, use /contact-import/:jwt with a JWT that has an array of contacts
// because that will do better error checking for things like missing data on iOS platforms.
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["contactJwt"] as string;
if (importedContactJwt) {
@@ -426,21 +449,25 @@ export default class ContactsView extends Vue {
const { payload } = decodeEndorserJwt(importedContactJwt);
const userInfo = payload["own"] as UserInfo;
const newContact = {
did: payload["iss"],
did: userInfo.did || payload["iss"], // ".did" is reliable as of v 0.3.49
name: userInfo.name,
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
profileImageUrl: userInfo.profileImageUrl,
publicKeyBase64: userInfo.publicEncKey,
registered: userInfo.registered,
} as Contact;
this.addContact(newContact);
await this.addContact(newContact);
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
(this.$router as Router).push({ path: "/contacts" });
}
}
private async processInviteJwt() {
// handle an invite JWT sent via URL
const importedInviteJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["inviteJwt"] as string;
if (importedInviteJwt === "") {
// this happens when a platform (usually iOS) doesn't include anything after the "=" in a shared link.
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
this.$notify(
{
group: "alert",
@@ -539,9 +566,15 @@ export default class ContactsView extends Vue {
5000,
);
}
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
(this.$router as Router).push({ path: "/contacts" });
}
}
private contactNameNonBreakingSpace(contactName?: string) {
return (contactName || AppString.NO_CONTACT_NAME).replace(/\s/g, "\u00A0");
}
private danger(message: string, title: string = "Error", timeout = 5000) {
this.$notify(
{
@@ -624,7 +657,7 @@ export default class ContactsView extends Vue {
(useRecipient ? "given" : "received") +
" data from the server.",
},
5000,
3000,
);
}
};
@@ -682,7 +715,7 @@ export default class ContactsView extends Vue {
title: "Load Error",
text: "Got an error loading your gives.",
},
5000,
3000,
);
}
}
@@ -697,8 +730,30 @@ export default class ContactsView extends Vue {
return;
}
if (contactInput.startsWith(CONTACT_URL_PREFIX)) {
await this.addContactFromScan(contactInput);
if (contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
const jwt = getContactJwtFromJwtUrl(contactInput);
(this.$router as Router).push({
path: "/contact-import/" + jwt,
});
return;
}
if (
contactInput.includes(CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI) ||
contactInput.includes(CONTACT_URL_PATH_ENDORSER_CH_OLD)
) {
const jwt = getContactJwtFromJwtUrl(contactInput);
const { payload } = decodeEndorserJwt(jwt);
const userInfo = payload["own"] as UserInfo;
const newContact = {
did: userInfo.did || payload["iss"], // "did" is reliable as of v 0.3.49
name: userInfo.name,
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
profileImageUrl: userInfo.profileImageUrl,
publicKeyBase64: userInfo.publicEncKey,
registered: userInfo.registered,
} as Contact;
await this.addContact(newContact);
return;
}
@@ -846,31 +901,6 @@ export default class ContactsView extends Vue {
return db.contacts.add(newContact);
}
private async addContactFromScan(url: string): Promise<void> {
const payload = getContactPayloadFromJwtUrl(url);
if (!payload) {
this.$notify(
{
group: "alert",
type: "danger",
title: "No Contact Info",
text: "The contact info could not be parsed.",
},
3000,
);
return;
} else {
return this.addContact({
did: payload.iss,
name: payload.own.name,
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
profileImageUrl: payload.own.profileImageUrl,
publicKeyBase64: payload.own.publicEncKey,
registered: payload.own.registered,
} as Contact);
}
}
private async addContact(newContact: Contact) {
if (!newContact.did) {
this.danger("Cannot add a contact without a DID.", "Incomplete Contact");
@@ -930,7 +960,7 @@ export default class ContactsView extends Vue {
},
-1,
);
}, 500);
}, 1000);
}
}
this.$notify(
@@ -958,7 +988,7 @@ export default class ContactsView extends Vue {
message +=
" Check that the contact doesn't conflict with any you already have.";
}
this.danger(message, "Contact Not Added", -1);
this.danger(message, "Contact Not Added", 5000);
});
}
@@ -1007,7 +1037,7 @@ export default class ContactsView extends Vue {
text:
(contact.name || "That unnamed person") + " has been registered.",
},
5000,
3000,
);
} else {
this.$notify(
@@ -1266,13 +1296,29 @@ export default class ContactsView extends Vue {
this.danger("You must select contacts to copy.");
return;
}
const selectedContacts = this.contacts.filter((c) =>
const selectedContactsFull = this.contacts.filter((c) =>
this.contactsSelected.includes(c.did),
);
console.log(
"Array of selected contacts:",
JSON.stringify(selectedContacts),
);
const selectedContacts: Array<Contact> = selectedContactsFull.map((c) => {
const contact: Contact = {
did: c.did,
name: c.name,
};
if (c.nextPubKeyHashB64) {
contact.nextPubKeyHashB64 = c.nextPubKeyHashB64;
}
if (c.profileImageUrl) {
contact.profileImageUrl = c.profileImageUrl;
}
if (c.publicKeyBase64) {
contact.publicKeyBase64 = c.publicKeyBase64;
}
return contact;
});
// console.log(
// "Array of selected contacts:",
// JSON.stringify(selectedContacts),
// );
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
contacts: selectedContacts,
});
@@ -1287,7 +1333,7 @@ export default class ContactsView extends Vue {
title: "Copied",
text: "The link for those contacts is now in the clipboard.",
},
5000,
3000,
);
});
}
@@ -1306,5 +1352,17 @@ export default class ContactsView extends Vue {
return did.substring(0, did.indexOf(":", 4) + 7) + "...";
}
}
private showCopySelectionsInfo() {
this.$notify(
{
group: "alert",
type: "info",
title: "Copying Contacts",
text: "Contact info will include name, ID, profile image, and public key.",
},
5000,
);
}
}
</script>