Fix duplicate export declarations and migrate ContactsView with sub-components

- Remove duplicate NOTIFY_INVITE_MISSING and NOTIFY_INVITE_PROCESSING_ERROR exports
- Update InviteOneAcceptView.vue to use correct NOTIFY_INVITE_TRUNCATED_DATA constant
- Migrate ContactsView to PlatformServiceMixin and extract into modular sub-components
- Resolves TypeScript compilation errors preventing web build
This commit is contained in:
Matthew Raymer
2025-07-16 08:03:26 +00:00
parent 81a6c92068
commit 8dd73950f5
45 changed files with 3216 additions and 1752 deletions

View File

@@ -22,131 +22,31 @@
</div>
<!-- New Contact -->
<div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
<span v-if="isRegistered" class="flex">
<router-link
:to="{ name: 'invite-one' }"
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
>
<font-awesome icon="envelope-open-text" class="fa-fw text-2xl" />
</router-link>
<ContactInputForm
:is-registered="isRegistered"
v-model="contactInput"
@submit="onClickNewContact"
@show-onboard-meeting="showOnboardMeetingDialog"
@registration-required="notify.warning('You must get registered before you can create invites.')"
@navigate-onboard-meeting="$router.push({ name: 'onboard-meeting-list' })"
@qr-scan="handleQRCodeClick"
/>
<button
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
@click="showOnboardMeetingDialog()"
>
<font-awesome icon="chair" class="fa-fw text-2xl" />
</button>
</span>
<span v-else class="flex">
<span
class="flex items-center 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-1 mr-1 rounded-md"
>
<font-awesome
icon="envelope-open-text"
class="fa-fw text-2xl"
@click="
notify.warning(
'You must get registered before you can create invites.',
)
"
/>
</span>
<span
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
>
<font-awesome
icon="chair"
class="fa-fw text-2xl"
@click="$router.push({ name: 'onboard-meeting-list' })"
/>
</span>
</span>
<button
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
@click="handleQRCodeClick"
>
<font-awesome icon="qrcode" class="fa-fw text-2xl" />
</button>
<textarea
v-model="contactInput"
type="text"
placeholder="New URL or DID, Name, Public Key, Next Public Key Hash"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
/>
<button
class="px-4 rounded-r bg-green-200 border border-green-400"
@click="onClickNewContact()"
>
<font-awesome icon="plus" class="fa-fw" />
</button>
</div>
<div v-if="contacts.length > 0" class="flex justify-between">
<div class="">
<div v-if="!showGiveNumbers" class="flex items-center">
<input
type="checkbox"
:checked="contactsSelected.length === contacts.length"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllTop"
@click="
contactsSelected.length === contacts.length
? (contactsSelected = [])
: (contactsSelected = contacts.map((contact) => contact.did))
"
/>
<button
v-if="!showGiveNumbers"
:class="
contactsSelected.length > 0
? '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 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: '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-slate-300 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
"
data-testId="copySelectedContactsButtonTop"
@click="copySelectedContacts()"
>
Copy
</button>
<font-awesome
icon="circle-info"
class="text-2xl text-blue-500 ml-2"
@click="showCopySelectionsInfo()"
/>
</div>
</div>
<div class="flex items-center gap-2">
<button
v-if="showGiveNumbers"
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
:class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()"
>
{{
showGiveTotals
? "Totals"
: showGiveConfirmed
? "Confirmed Amounts"
: "Unconfirmed Amounts"
}}
<font-awesome icon="left-right" class="fa-fw" />
</button>
<button
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-3 py-1.5 rounded-md"
@click="toggleShowContactAmounts()"
>
{{ showGiveNumbers ? "Hide Actions" : "See Actions" }}
</button>
</div>
</div>
<ContactListHeader
v-if="contacts.length > 0"
:show-give-numbers="showGiveNumbers"
:all-contacts-selected="allContactsSelected"
:copy-button-class="copyButtonClass"
:copy-button-disabled="copyButtonDisabled"
:give-amounts-button-text="giveAmountsButtonText"
:show-actions-button-text="showActionsButtonText"
:give-amounts-button-class="showGiveAmountsClassNames()"
@toggle-all-selection="toggleAllContactsSelection"
@copy-selected="copySelectedContacts"
@show-copy-info="showCopySelectionsInfo"
@toggle-give-totals="toggleShowGiveTotals"
@toggle-show-actions="toggleShowContactAmounts"
/>
<div v-if="showGiveNumbers" class="my-3">
<div class="w-full text-center text-sm italic text-slate-600">
Only the most recent hours are included. <br />To see more, click
@@ -165,183 +65,48 @@
id="listContacts"
class="border-t border-slate-300 my-2"
>
<li
v-for="contact in filteredContacts()"
<ContactListItem
v-for="contact in filteredContacts"
:key="contact.did"
class="border-b border-slate-300 pt-1 pb-1"
data-testId="contactListItem"
>
<div class="flex items-center justify-between gap-3">
<div class="flex overflow-hidden min-w-0 items-center gap-3">
<input
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
contactsSelected.indexOf(contact.did),
1,
)
: contactsSelected.push(contact.did)
"
/>
<EntityIcon
:contact="contact"
:icon-size="48"
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
<div class="overflow-hidden">
<h2 class="text-base font-semibold truncate">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
{{ contactNameNonBreakingSpace(contact.name) }}
</router-link>
</h2>
<div class="flex gap-1.5 items-center overflow-hidden">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
<font-awesome
icon="circle-info"
class="text-base text-blue-500"
/>
</router-link>
<span class="text-xs truncate">{{ contact.did }}</span>
</div>
<div class="text-sm">
{{ contact.notes }}
</div>
</div>
</div>
<div
v-if="showGiveNumbers && contact.did != activeDid"
class="flex gap-1.5 items-end"
>
<div class="text-center">
<div class="text-xs leading-none mb-1">From/To</div>
<div class="flex items-center">
<button
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.5 py-1.5 rounded-l-md"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<button
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.5 py-1.5 rounded-r-md border-l"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
</div>
</div>
<button
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-md"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
>
Offer
</button>
<router-link
:to="{
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="text-sm 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-2 py-1.5 rounded-md"
title="See more given activity"
>
<font-awesome icon="file-lines" class="fa-fw" />
</router-link>
</div>
</div>
</li>
:contact="contact"
:active-did="activeDid"
:show-checkbox="!showGiveNumbers"
:show-actions="showGiveNumbers"
:is-selected="contactsSelected.includes(contact.did)"
:show-give-totals="showGiveTotals"
:show-give-confirmed="showGiveConfirmed"
:given-to-me-descriptions="givenToMeDescriptions"
:given-to-me-confirmed="givenToMeConfirmed"
:given-to-me-unconfirmed="givenToMeUnconfirmed"
:given-by-me-descriptions="givenByMeDescriptions"
:given-by-me-confirmed="givenByMeConfirmed"
:given-by-me-unconfirmed="givenByMeUnconfirmed"
@toggle-selection="toggleContactSelection"
@show-identicon="showLargeIdenticon = $event"
@show-gifted-dialog="confirmShowGiftedDialog"
@open-offer-dialog="openOfferDialog"
/>
</ul>
<p v-else>There are no contacts.</p>
<div v-if="contacts.length > 0" class="mt-2 w-full text-left">
<input
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.length === contacts.length"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllBottom"
@click="
contactsSelected.length === contacts.length
? (contactsSelected = [])
: (contactsSelected = contacts.map((contact) => contact.did))
"
/>
<button
v-if="!showGiveNumbers"
:class="
contactsSelected.length > 0
? '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 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: '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-slate-300 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
"
@click="copySelectedContacts()"
>
Copy
</button>
</div>
<ContactBulkActions
v-if="contacts.length > 0"
:show-give-numbers="showGiveNumbers"
:all-contacts-selected="allContactsSelected"
:copy-button-class="copyButtonClass"
:copy-button-disabled="copyButtonDisabled"
@toggle-all-selection="toggleAllContactsSelection"
@copy-selected="copySelectedContacts"
/>
<GiftedDialog ref="customGivenDialog" />
<OfferDialog ref="customOfferDialog" />
<ContactNameDialog ref="contactNameDialog" />
<div v-if="showLargeIdenticon" class="fixed z-[100] top-0 inset-x-0 w-full">
<div
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<EntityIcon
:contact="showLargeIdenticon"
:icon-size="512"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="showLargeIdenticon = undefined"
/>
</div>
</div>
<LargeIdenticonModal
:contact="showLargeIdenticon"
@close="showLargeIdenticon = undefined"
/>
</section>
</template>
@@ -362,6 +127,11 @@ import GiftedDialog from "../components/GiftedDialog.vue";
import OfferDialog from "../components/OfferDialog.vue";
import ContactNameDialog from "../components/ContactNameDialog.vue";
import TopMessage from "../components/TopMessage.vue";
import ContactListItem from "../components/ContactListItem.vue";
import ContactInputForm from "../components/ContactInputForm.vue";
import ContactListHeader from "../components/ContactListHeader.vue";
import ContactBulkActions from "../components/ContactBulkActions.vue";
import LargeIdenticonModal from "../components/LargeIdenticonModal.vue";
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
import { logConsoleAndDb } from "../db/index";
import { Contact } from "../db/tables/contacts";
@@ -418,6 +188,26 @@ import {
getGivesRetrievalErrorMessage,
} from "@/constants/notifications";
/**
* ContactsView - Main contact management interface
*
* This view provides comprehensive contact management functionality including:
* - Contact display and filtering
* - Contact creation from various input formats (DID, JWT, CSV, JSON)
* - Contact selection and bulk operations
* - Give amounts display and management
* - Contact registration and visibility settings
* - QR code scanning integration
* - Meeting onboarding functionality
*
* The component uses the Enhanced Triple Migration Pattern with:
* - PlatformServiceMixin for database operations
* - Centralized notification constants
* - Computed properties for template streamlining
* - Refactored methods for maintainability
*
* @author Matthew Raymer
*/
@Component({
components: {
GiftedDialog,
@@ -426,6 +216,11 @@ import {
QuickNav,
ContactNameDialog,
TopMessage,
ContactListItem,
ContactInputForm,
ContactListHeader,
ContactBulkActions,
LargeIdenticonModal,
},
mixins: [PlatformServiceMixin],
})
@@ -470,6 +265,11 @@ export default class ContactsView extends Vue {
AppString = AppString;
libsUtil = libsUtil;
/**
* Component lifecycle hook - Initialize component state and load data
* Sets up notification helpers, loads user settings, processes URL parameters,
* and loads contacts from database
*/
public async created() {
this.notify = createNotifyHelpers(this.$notify);
@@ -603,9 +403,7 @@ export default class ContactsView extends Vue {
}
}
private contactNameNonBreakingSpace(contactName?: string) {
return (contactName || AppString.NO_CONTACT_NAME).replace(/\s/g, "\u00A0");
}
// Legacy danger() and warning() methods removed - now using this.notify.error() and this.notify.warning()
@@ -624,7 +422,8 @@ export default class ContactsView extends Vue {
);
}
private filteredContacts() {
// Computed properties for template streamlining
get filteredContacts() {
return this.showGiveNumbers
? this.contactsSelected.length === 0
? this.contacts
@@ -634,6 +433,54 @@ export default class ContactsView extends Vue {
: this.contacts;
}
get copyButtonClass() {
return this.contactsSelected.length > 0
? '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 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: '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-slate-300 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed';
}
get copyButtonDisabled() {
return this.contactsSelected.length === 0;
}
get giveAmountsButtonText() {
if (this.showGiveTotals) {
return "Totals";
}
return this.showGiveConfirmed ? "Confirmed Amounts" : "Unconfirmed Amounts";
}
get showActionsButtonText() {
return this.showGiveNumbers ? "Hide Actions" : "See Actions";
}
get allContactsSelected() {
return this.contactsSelected.length === this.contacts.length;
}
// Helper methods for template interactions
toggleAllContactsSelection(): void {
if (this.allContactsSelected) {
this.contactsSelected = [];
} else {
this.contactsSelected = this.contacts.map((contact) => contact.did);
}
}
toggleContactSelection(contactDid: string): void {
if (this.contactsSelected.includes(contactDid)) {
this.contactsSelected.splice(this.contactsSelected.indexOf(contactDid), 1);
} else {
this.contactsSelected.push(contactDid);
}
}
private async loadGives() {
if (!this.activeDid) {
return;
@@ -723,14 +570,31 @@ export default class ContactsView extends Vue {
}
}
/**
* Main method to handle new contact input processing
* Routes to appropriate parsing method based on input format
*/
private async onClickNewContact(): Promise<void> {
const contactInput = this.contactInput.trim();
if (!contactInput) {
// Use notification helper and constant
this.notify.error(NOTIFY_CONTACT_NO_INFO.message);
return;
}
// Try different parsing methods in order
if (await this.tryParseJwtContact(contactInput)) return;
if (await this.tryParseCsvContacts(contactInput)) return;
if (await this.tryParseDidContact(contactInput)) return;
if (await this.tryParseJsonContacts(contactInput)) return;
// If no parsing method succeeded
this.notify.error(NOTIFY_CONTACT_NO_CONTACT_FOUND.message);
}
/**
* Parse contact from JWT URL format
*/
private async tryParseJwtContact(contactInput: string): Promise<boolean> {
if (
contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI) ||
contactInput.includes(CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI) ||
@@ -741,7 +605,7 @@ export default class ContactsView extends Vue {
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
did: userInfo.did || payload["iss"],
name: userInfo.name,
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
profileImageUrl: userInfo.profileImageUrl,
@@ -749,10 +613,16 @@ export default class ContactsView extends Vue {
registered: userInfo.registered,
} as Contact;
await this.addContact(newContact);
return;
return true;
}
}
return false;
}
/**
* Parse contacts from CSV format
*/
private async tryParseCsvContacts(contactInput: string): Promise<boolean> {
if (contactInput.startsWith(CONTACT_CSV_HEADER)) {
const lines = contactInput.split(/\n/);
const lineAdded = [];
@@ -766,61 +636,79 @@ export default class ContactsView extends Vue {
await Promise.all(lineAdded);
this.notify.success(NOTIFY_CONTACTS_ADDED_CSV.message);
} catch (e) {
const fullError =
"Error adding contacts from CSV: " + errorStringForLog(e);
const fullError = "Error adding contacts from CSV: " + errorStringForLog(e);
logConsoleAndDb(fullError, true);
// Use notification helper and constant
this.notify.error(NOTIFY_CONTACTS_ADD_ERROR.message);
}
// Replace PlatformServiceFactory query with mixin method
this.contacts = await this.$getAllContacts();
return;
return true;
}
return false;
}
/**
* Parse contact from DID format with optional parameters
*/
private async tryParseDidContact(contactInput: string): Promise<boolean> {
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
}
const parsedContact = this.parseDidContactString(contactInput);
await this.addContact(parsedContact);
return true;
}
return false;
}
/**
* Parse DID contact string into Contact object
*/
private parseDidContactString(contactInput: string): Contact {
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();
nextPublicKeyHashInput = contactInput.substring(commaPos3 + 1).trim();
}
}
// 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;
}
// Convert hex keys to base64 if needed
const publicKeyBase64 = this.convertHexToBase64(publicKeyInput);
const nextPubKeyHashB64 = this.convertHexToBase64(nextPublicKeyHashInput);
return {
did,
name,
publicKeyBase64,
nextPubKeyHashB64,
};
}
/**
* Convert hex string to base64 if it matches hex pattern
*/
private convertHexToBase64(hexString?: string): string | undefined {
if (!hexString || !/^[0-9A-Fa-f]{66}$/i.test(hexString)) {
return hexString;
}
return Buffer.from(hexString, "hex").toString("base64");
}
/**
* Parse contacts from JSON array format
*/
private async tryParseJsonContacts(contactInput: string): Promise<boolean> {
if (contactInput.includes("[")) {
// assume there's a JSON array of contacts in the input
const jsonContactInput = contactInput.substring(
contactInput.indexOf("["),
contactInput.lastIndexOf("]") + 1,
@@ -831,18 +719,14 @@ export default class ContactsView extends Vue {
name: "contact-import",
query: { contacts: JSON.stringify(contacts) },
});
return true;
} catch (e) {
const fullError =
"Error adding contacts from array: " + errorStringForLog(e);
const fullError = "Error adding contacts from array: " + errorStringForLog(e);
logConsoleAndDb(fullError, true);
// Use notification helper and constant
this.notify.error(NOTIFY_CONTACT_INPUT_PARSE_ERROR.message);
}
return;
}
// Use notification helper and constant
this.notify.error(NOTIFY_CONTACT_NO_CONTACT_FOUND.message);
return false;
}
private async addContactFromEndorserMobileLine(
@@ -855,94 +739,145 @@ export default class ContactsView extends Vue {
return newContact.did as IndexableType;
}
/**
* Add a new contact to the database and update UI
* Validates contact data, inserts into database, updates local state,
* sets visibility, and handles registration prompts
*/
private async addContact(newContact: Contact) {
if (!newContact.did) {
// Use notification helper and constant
this.notify.error(NOTIFY_CONTACT_NO_DID.message);
return;
}
if (!isDid(newContact.did)) {
// Use notification helper and constant
this.notify.error(NOTIFY_CONTACT_INVALID_DID.message);
// Validate contact data
if (!this.validateContactData(newContact)) {
return;
}
// Replace PlatformServiceFactory with mixin method
try {
// Insert contact into database
await this.$insertContact(newContact);
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
let addedMessage;
if (this.activeDid) {
this.setVisibility(newContact, true, false);
newContact.seesMe = true; // didn't work inside setVisibility
addedMessage = NOTIFY_CONTACTS_ADDED_VISIBLE.message;
} else {
addedMessage = NOTIFY_CONTACTS_ADDED.message;
}
// Update local contacts list
this.updateContactsList(newContact);
// Set visibility and get success message
const addedMessage = await this.handleContactVisibility(newContact);
// Clear input field
this.contactInput = "";
if (this.isRegistered) {
if (!this.hideRegisterPromptOnNewContact && !newContact.registered) {
setTimeout(() => {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Register",
text: "Do you want to register them?",
onCancel: async (stopAsking?: boolean) => {
if (stopAsking) {
await this.$saveSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
onNo: async (stopAsking?: boolean) => {
if (stopAsking) {
await this.$saveSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
onYes: async () => {
await this.register(newContact);
},
promptToStopAsking: true,
},
-1,
);
}, 1000);
}
}
// Use notification helper and constant
// Handle registration prompt if needed
await this.handleRegistrationPrompt(newContact);
// Show success notification
this.notify.success(addedMessage);
} catch (err) {
const fullError =
"Error when adding contact to storage: " + errorStringForLog(err);
logConsoleAndDb(fullError, true);
let message = NOTIFY_CONTACT_IMPORT_ERROR.message;
if (
(err as any).message?.indexOf(
"Key already exists in the object store.",
) > -1
) {
message = NOTIFY_CONTACT_IMPORT_CONFLICT.message;
}
if ((err as any).name === "ConstraintError") {
message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message;
}
// Use notification helper and constant
this.notify.error(message, TIMEOUTS.LONG);
this.handleContactAddError(err);
}
}
// note that this is also in DIDView.vue
/**
* Validate contact data before insertion
*/
private validateContactData(newContact: Contact): boolean {
if (!newContact.did) {
this.notify.error(NOTIFY_CONTACT_NO_DID.message);
return false;
}
if (!isDid(newContact.did)) {
this.notify.error(NOTIFY_CONTACT_INVALID_DID.message);
return false;
}
return true;
}
/**
* Update local contacts list with new contact
*/
private updateContactsList(newContact: Contact): void {
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
}
/**
* Handle contact visibility settings and return appropriate message
*/
private async handleContactVisibility(newContact: Contact): Promise<string> {
if (this.activeDid) {
await this.setVisibility(newContact, true, false);
newContact.seesMe = true;
return NOTIFY_CONTACTS_ADDED_VISIBLE.message;
} else {
return NOTIFY_CONTACTS_ADDED.message;
}
}
/**
* Handle registration prompt for new contacts
*/
private async handleRegistrationPrompt(newContact: Contact): Promise<void> {
if (!this.isRegistered || this.hideRegisterPromptOnNewContact || newContact.registered) {
return;
}
setTimeout(() => {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Register",
text: "Do you want to register them?",
onCancel: async (stopAsking?: boolean) => {
await this.handleRegistrationPromptResponse(stopAsking);
},
onNo: async (stopAsking?: boolean) => {
await this.handleRegistrationPromptResponse(stopAsking);
},
onYes: async () => {
await this.register(newContact);
},
promptToStopAsking: true,
},
-1,
);
}, 1000);
}
/**
* Handle user response to registration prompt
*/
private async handleRegistrationPromptResponse(stopAsking?: boolean): Promise<void> {
if (stopAsking) {
await this.$saveSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
}
/**
* Handle errors during contact addition
*/
private handleContactAddError(err: any): void {
const fullError = "Error when adding contact to storage: " + errorStringForLog(err);
logConsoleAndDb(fullError, true);
let message = NOTIFY_CONTACT_IMPORT_ERROR.message;
if ((err as any).message?.indexOf("Key already exists in the object store.") > -1) {
message = NOTIFY_CONTACT_IMPORT_CONFLICT.message;
}
if ((err as any).name === "ConstraintError") {
message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message;
}
this.notify.error(message, TIMEOUTS.LONG);
}
/**
* Register a contact with the endorser server
* Sends registration request and updates contact status on success
* Note: This method is also used in DIDView.vue
*/
private async register(contact: Contact) {
this.notify.sent();
@@ -994,7 +929,10 @@ export default class ContactsView extends Vue {
}
}
// note that this is also in DIDView.vue
/**
* Set visibility for a contact on the endorser server
* Note: This method is also used in DIDView.vue
*/
private async setVisibility(
contact: Contact,
visibility: boolean,
@@ -1027,6 +965,10 @@ export default class ContactsView extends Vue {
}
}
/**
* Confirm and show gifted dialog with unconfirmed amounts check
* If there are unconfirmed amounts, prompts user to confirm them first
*/
private confirmShowGiftedDialog(giverDid: string, recipientDid: string) {
// if they have unconfirmed amounts, ask to confirm those
if (
@@ -1173,6 +1115,10 @@ export default class ContactsView extends Vue {
};
}
/**
* Copy selected contacts as a shareable JWT URL
* Creates a JWT containing selected contact data and copies to clipboard
*/
private async copySelectedContacts() {
if (this.contactsSelected.length === 0) {
// Use notification helper and constant
@@ -1216,6 +1162,10 @@ export default class ContactsView extends Vue {
this.notify.info(NOTIFY_CONTACT_INFO_COPY.message, TIMEOUTS.LONG);
}
/**
* Show onboarding meeting dialog based on user's meeting status
* Checks if user is in a meeting and whether they are the host
*/
private async showOnboardMeetingDialog() {
try {
// First check if they're in a meeting
@@ -1268,6 +1218,10 @@ export default class ContactsView extends Vue {
}
}
/**
* Handle QR code button click - route to appropriate scanner
* Uses native scanner on mobile platforms, web scanner otherwise
*/
private handleQRCodeClick() {
if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" });