forked from jsnbuchanan/crowd-funder-for-time-pwa
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:
@@ -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" });
|
||||
|
||||
Reference in New Issue
Block a user