diff --git a/src/composables/useNotifications.ts b/src/composables/useNotifications.ts new file mode 100644 index 00000000..deddd3c5 --- /dev/null +++ b/src/composables/useNotifications.ts @@ -0,0 +1,140 @@ +import { inject } from 'vue'; +import { NotificationIface } from '../constants/app'; + +/** + * Vue 3 composable for notifications + * Provides a concise API for common notification patterns + */ + +export const NOTIFICATION_TIMEOUTS = { + BRIEF: 1000, // Very brief toasts ("Sent..." messages) + SHORT: 2000, // Short notifications (clipboard copies, quick confirmations) + STANDARD: 3000, // Standard notifications (success messages, general info) + LONG: 5000, // Longer notifications (errors, warnings, important info) + VERY_LONG: 7000, // Very long notifications (complex operations) + MODAL: -1, // Modal confirmations (no auto-dismiss) +} as const; + +export function useNotifications() { + // Inject the notify function from the app + const notify = inject<(notification: NotificationIface, timeout?: number) => void>('$notify'); + + if (!notify) { + throw new Error('useNotifications must be used within a component that has $notify available'); + } + + return { + // Direct access to the original notify function + notify, + + // Success notifications + success: (text: string, timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notify({ + group: "alert", + type: "success", + title: "Success", + text, + }, timeout); + }, + + // Error notifications + error: (text: string, timeout = NOTIFICATION_TIMEOUTS.LONG) => { + notify({ + group: "alert", + type: "danger", + title: "Error", + text, + }, timeout); + }, + + // Warning notifications + warning: (text: string, timeout = NOTIFICATION_TIMEOUTS.LONG) => { + notify({ + group: "alert", + type: "warning", + title: "Warning", + text, + }, timeout); + }, + + // Info notifications + info: (text: string, timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notify({ + group: "alert", + type: "info", + title: "Info", + text, + }, timeout); + }, + + // Toast notifications (brief) + toast: (title: string, text?: string, timeout = NOTIFICATION_TIMEOUTS.BRIEF) => { + notify({ + group: "alert", + type: "toast", + title, + text, + }, timeout); + }, + + // Clipboard copy notifications + copied: (item: string, timeout = NOTIFICATION_TIMEOUTS.SHORT) => { + notify({ + group: "alert", + type: "toast", + title: "Copied", + text: `${item} was copied to the clipboard.`, + }, timeout); + }, + + // Sent brief notification + sent: (timeout = NOTIFICATION_TIMEOUTS.BRIEF) => { + notify({ + group: "alert", + type: "toast", + title: "Sent...", + }, timeout); + }, + + // Confirmation modal + confirm: (text: string, onYes: () => Promise, timeout = NOTIFICATION_TIMEOUTS.MODAL) => { + notify({ + group: "modal", + type: "confirm", + title: "Confirm", + text, + onYes, + }, timeout); + }, + + // Standard confirmation messages + confirmationSubmitted: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notify({ + group: "alert", + type: "success", + title: "Success", + text: "Confirmation submitted.", + }, timeout); + }, + + // Common error patterns + genericError: (timeout = NOTIFICATION_TIMEOUTS.LONG) => { + notify({ + group: "alert", + type: "danger", + title: "Error", + text: "Something went wrong.", + }, timeout); + }, + + // Common success patterns + genericSuccess: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notify({ + group: "alert", + type: "success", + title: "Success", + text: "Operation completed successfully.", + }, timeout); + }, + }; +} \ No newline at end of file diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index 543314d3..ae070650 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -545,8 +545,8 @@ export const PlatformServiceMixin = { /** * Load all contacts with caching - $contacts() - * Ultra-concise shortcut with 60s TTL for performance - * @returns Cached mapped array of all contacts + * Contacts are cached for 60 seconds for performance + * @returns Promise Array of contact objects */ async $contacts(): Promise { const cacheKey = "contacts_all"; @@ -565,6 +565,16 @@ export const PlatformServiceMixin = { ); }, + /** + * Get total contact count - $contactCount() + * Ultra-concise shortcut for getting number of contacts + * @returns Promise Total number of contacts + */ + async $contactCount(): Promise { + const countRow = await this.$one("SELECT COUNT(*) FROM contacts"); + return (countRow?.[0] as number) || 0; + }, + /** * Load settings with optional defaults WITHOUT caching - $settings() * Settings are loaded fresh every time for immediate consistency @@ -1136,6 +1146,7 @@ export interface IPlatformServiceMixin { $insertContact(contact: Partial): Promise; $updateContact(did: string, changes: Partial): Promise; $getAllContacts(): Promise; + $contactCount(): Promise; $insertEntity( tableName: string, entity: Record, @@ -1211,6 +1222,7 @@ declare module "@vue/runtime-core" { // Specialized shortcuts - contacts cached, settings fresh $contacts(): Promise; + $contactCount(): Promise; $settings(defaults?: Settings): Promise; $accountSettings(did?: string, defaults?: Settings): Promise; diff --git a/src/utils/notificationUtils.ts b/src/utils/notificationUtils.ts new file mode 100644 index 00000000..dc06e997 --- /dev/null +++ b/src/utils/notificationUtils.ts @@ -0,0 +1,223 @@ +import { NotificationIface } from "../constants/app"; + +/** + * Notification utility methods to reduce code duplication + * and provide consistent notification patterns across the app + */ + +export interface NotificationHelper { + notify: (notification: NotificationIface, timeout?: number) => void; + success: (text: string, timeout?: number) => void; + error: (text: string, timeout?: number) => void; + warning: (text: string, timeout?: number) => void; + info: (text: string, timeout?: number) => void; + toast: (title: string, text?: string, timeout?: number) => void; + copied: (item: string, timeout?: number) => void; + sent: (timeout?: number) => void; + confirm: (text: string, onYes: () => Promise, timeout?: number) => void; + confirmationSubmitted: (timeout?: number) => void; + alreadyConfirmed: (timeout?: number) => void; + cannotConfirmIssuer: (timeout?: number) => void; + cannotConfirmHidden: (timeout?: number) => void; + notRegistered: (timeout?: number) => void; + notAGive: (timeout?: number) => void; +} + +/** + * Standard notification timeouts + */ +export const NOTIFICATION_TIMEOUTS = { + BRIEF: 1000, // Very brief toasts ("Sent..." messages) + SHORT: 2000, // Short notifications (clipboard copies, quick confirmations) + STANDARD: 3000, // Standard notifications (success messages, general info) + LONG: 5000, // Longer notifications (errors, warnings, important info) + VERY_LONG: 7000, // Very long notifications (complex operations) + MODAL: -1, // Modal confirmations (no auto-dismiss) +} as const; + +/** + * Standard notification titles + */ +export const NOTIFICATION_TITLES = { + SUCCESS: "Success", + ERROR: "Error", + WARNING: "Warning", + INFO: "Info", + COPIED: "Copied", + SENT: "Sent...", + CONFIRM: "Confirm", + NOT_REGISTERED: "Not Registered", + ALREADY_CONFIRMED: "Already Confirmed", + CANNOT_CONFIRM: "Cannot Confirm", +} as const; + +/** + * Standard notification messages + */ +export const NOTIFICATION_MESSAGES = { + GENERIC_ERROR: "Something went wrong.", + GENERIC_SUCCESS: "Operation completed successfully.", + CLIPBOARD_COPIED: (item: string) => `${item} was copied to the clipboard.`, + SENT_BRIEF: "Sent...", + CONFIRMATION_SUBMITTED: "Confirmation submitted.", + ALREADY_CONFIRMED: "You already confirmed this claim.", + CANNOT_CONFIRM_ISSUER: "You cannot confirm this because you issued this claim.", + CANNOT_CONFIRM_HIDDEN: "You cannot confirm this because some people are hidden.", + NOT_REGISTERED: "Someone needs to register you before you can confirm.", + NOT_A_GIVE: "This is not a giving action to confirm.", +} as const; + +/** + * Creates a notification helper with utility methods + */ +export function createNotificationHelper(notifyFn: (notification: NotificationIface, timeout?: number) => void): NotificationHelper { + return { + notify: notifyFn, + + // Success notifications + success: (text: string, timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notifyFn({ + group: "alert", + type: "success", + title: NOTIFICATION_TITLES.SUCCESS, + text, + }, timeout); + }, + + // Error notifications + error: (text: string, timeout = NOTIFICATION_TIMEOUTS.LONG) => { + notifyFn({ + group: "alert", + type: "danger", + title: NOTIFICATION_TITLES.ERROR, + text, + }, timeout); + }, + + // Warning notifications + warning: (text: string, timeout = NOTIFICATION_TIMEOUTS.LONG) => { + notifyFn({ + group: "alert", + type: "warning", + title: NOTIFICATION_TITLES.WARNING, + text, + }, timeout); + }, + + // Info notifications + info: (text: string, timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notifyFn({ + group: "alert", + type: "info", + title: NOTIFICATION_TITLES.INFO, + text, + }, timeout); + }, + + // Toast notifications (brief) + toast: (title: string, text?: string, timeout = NOTIFICATION_TIMEOUTS.BRIEF) => { + notifyFn({ + group: "alert", + type: "toast", + title, + text, + }, timeout); + }, + + // Clipboard copy notifications + copied: (item: string, timeout = NOTIFICATION_TIMEOUTS.SHORT) => { + notifyFn({ + group: "alert", + type: "toast", + title: NOTIFICATION_TITLES.COPIED, + text: NOTIFICATION_MESSAGES.CLIPBOARD_COPIED(item), + }, timeout); + }, + + // Sent brief notification + sent: (timeout = NOTIFICATION_TIMEOUTS.BRIEF) => { + notifyFn({ + group: "alert", + type: "toast", + title: NOTIFICATION_TITLES.SENT, + }, timeout); + }, + + // Confirmation modal + confirm: (text: string, onYes: () => Promise, timeout = NOTIFICATION_TIMEOUTS.MODAL) => { + notifyFn({ + group: "modal", + type: "confirm", + title: NOTIFICATION_TITLES.CONFIRM, + text, + onYes, + }, timeout); + }, + + // Standard confirmation messages + confirmationSubmitted: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notifyFn({ + group: "alert", + type: "success", + title: NOTIFICATION_TITLES.SUCCESS, + text: NOTIFICATION_MESSAGES.CONFIRMATION_SUBMITTED, + }, timeout); + }, + + alreadyConfirmed: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notifyFn({ + group: "alert", + type: "info", + title: NOTIFICATION_TITLES.ALREADY_CONFIRMED, + text: NOTIFICATION_MESSAGES.ALREADY_CONFIRMED, + }, timeout); + }, + + cannotConfirmIssuer: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notifyFn({ + group: "alert", + type: "info", + title: NOTIFICATION_TITLES.CANNOT_CONFIRM, + text: NOTIFICATION_MESSAGES.CANNOT_CONFIRM_ISSUER, + }, timeout); + }, + + cannotConfirmHidden: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notifyFn({ + group: "alert", + type: "info", + title: NOTIFICATION_TITLES.CANNOT_CONFIRM, + text: NOTIFICATION_MESSAGES.CANNOT_CONFIRM_HIDDEN, + }, timeout); + }, + + notRegistered: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notifyFn({ + group: "alert", + type: "info", + title: NOTIFICATION_TITLES.NOT_REGISTERED, + text: NOTIFICATION_MESSAGES.NOT_REGISTERED, + }, timeout); + }, + + notAGive: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => { + notifyFn({ + group: "alert", + type: "info", + title: NOTIFICATION_TITLES.INFO, + text: NOTIFICATION_MESSAGES.NOT_A_GIVE, + }, timeout); + }, + }; +} + +/** + * Vue mixin to add notification helpers to components + */ +export const NotificationMixin = { + computed: { + $notifyHelper() { + return createNotificationHelper((this as any).$notify); + }, + }, +}; \ No newline at end of file diff --git a/src/utils/notify.ts b/src/utils/notify.ts new file mode 100644 index 00000000..a9205ac9 --- /dev/null +++ b/src/utils/notify.ts @@ -0,0 +1,136 @@ +import { NotificationIface } from '../constants/app'; + +/** + * Simple notification utility functions + * Provides the most concise API for common notification patterns + */ + +export type NotifyFunction = (notification: NotificationIface, timeout?: number) => void; + +// Standard timeouts +export const TIMEOUTS = { + BRIEF: 1000, // Very brief toasts ("Sent..." messages) + SHORT: 2000, // Short notifications (clipboard copies, quick confirmations) + STANDARD: 3000, // Standard notifications (success messages, general info) + LONG: 5000, // Longer notifications (errors, warnings, important info) + VERY_LONG: 7000, // Very long notifications (complex operations) + MODAL: -1, // Modal confirmations (no auto-dismiss) +} as const; + +/** + * Create notification helpers for a given notify function + */ +export function createNotifyHelpers(notify: NotifyFunction) { + return { + // Success notifications + success: (text: string, timeout?: number) => + notify({ group: "alert", type: "success", title: "Success", text }, timeout || TIMEOUTS.STANDARD), + + // Error notifications + error: (text: string, timeout?: number) => + notify({ group: "alert", type: "danger", title: "Error", text }, timeout || TIMEOUTS.LONG), + + // Warning notifications + warning: (text: string, timeout?: number) => + notify({ group: "alert", type: "warning", title: "Warning", text }, timeout || TIMEOUTS.LONG), + + // Info notifications + info: (text: string, timeout?: number) => + notify({ group: "alert", type: "info", title: "Info", text }, timeout || TIMEOUTS.STANDARD), + + // Toast notifications (brief) + toast: (title: string, text?: string, timeout?: number) => + notify({ group: "alert", type: "toast", title, text }, timeout || TIMEOUTS.BRIEF), + + // Clipboard copy notifications + copied: (item: string, timeout?: number) => + notify({ + group: "alert", + type: "toast", + title: "Copied", + text: `${item} was copied to the clipboard.` + }, timeout || TIMEOUTS.SHORT), + + // Sent brief notification + sent: (timeout?: number) => + notify({ group: "alert", type: "toast", title: "Sent..." }, timeout || TIMEOUTS.BRIEF), + + // Confirmation modal + confirm: (text: string, onYes: () => Promise, timeout?: number) => + notify({ + group: "modal", + type: "confirm", + title: "Confirm", + text, + onYes + }, timeout || TIMEOUTS.MODAL), + + // Standard confirmation messages + confirmationSubmitted: (timeout?: number) => + notify({ + group: "alert", + type: "success", + title: "Success", + text: "Confirmation submitted." + }, timeout || TIMEOUTS.STANDARD), + + // Common error patterns + genericError: (timeout?: number) => + notify({ + group: "alert", + type: "danger", + title: "Error", + text: "Something went wrong." + }, timeout || TIMEOUTS.LONG), + + // Common success patterns + genericSuccess: (timeout?: number) => + notify({ + group: "alert", + type: "success", + title: "Success", + text: "Operation completed successfully." + }, timeout || TIMEOUTS.STANDARD), + + // Common confirmation patterns + alreadyConfirmed: (timeout?: number) => + notify({ + group: "alert", + type: "info", + title: "Already Confirmed", + text: "You already confirmed this claim." + }, timeout || TIMEOUTS.STANDARD), + + cannotConfirmIssuer: (timeout?: number) => + notify({ + group: "alert", + type: "info", + title: "Cannot Confirm", + text: "You cannot confirm this because you issued this claim." + }, timeout || TIMEOUTS.STANDARD), + + cannotConfirmHidden: (timeout?: number) => + notify({ + group: "alert", + type: "info", + title: "Cannot Confirm", + text: "You cannot confirm this because some people are hidden." + }, timeout || TIMEOUTS.STANDARD), + + notRegistered: (timeout?: number) => + notify({ + group: "alert", + type: "info", + title: "Not Registered", + text: "Someone needs to register you before you can confirm." + }, timeout || TIMEOUTS.STANDARD), + + notAGive: (timeout?: number) => + notify({ + group: "alert", + type: "info", + title: "Info", + text: "This is not a giving action to confirm." + }, timeout || TIMEOUTS.STANDARD), + }; +} \ No newline at end of file diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index b3c56b02..f658de99 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -531,6 +531,7 @@ import { } from "../interfaces"; import * as libsUtil from "../libs/util"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; +import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; @Component({ components: { GiftedDialog, QuickNav }, @@ -577,6 +578,9 @@ export default class ClaimView extends Vue { libsUtil = libsUtil; serverUtil = serverUtil; + // Add notification helpers + private notify = createNotifyHelpers(this.$notify); + // ================================================= // COMPUTED PROPERTIES // ================================================= @@ -728,30 +732,14 @@ export default class ClaimView extends Vue { "Error retrieving all account DIDs on home page:" + error, true, ); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error Loading Profile", - text: "See the Help page for problems with your personal data.", - }, - 5000, - ); + this.notify.error("See the Help page for problems with your personal data."); } const claimId = this.$route.params.id as string; if (claimId) { await this.loadClaim(claimId, this.activeDid); } else { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "No claim ID was provided.", - }, - 5000, - ); + this.notify.error("No claim ID was provided."); } this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`; @@ -803,15 +791,7 @@ export default class ClaimView extends Vue { } else { // actually, axios typically throws an error so we never get here await this.$logError("Error getting claim: " + JSON.stringify(resp)); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "There was a problem retrieving that claim.", - }, - 5000, - ); + this.notify.error("There was a problem retrieving that claim."); return; } @@ -854,15 +834,7 @@ export default class ClaimView extends Vue { await this.$logError( "Error getting give providers: " + JSON.stringify(giveResp), ); - this.$notify( - { - group: "alert", - type: "warning", - title: "Error", - text: "Got error retrieving linked provider data.", - }, - 5000, - ); + this.notify.warning("Got error retrieving linked provider data."); } } else if (this.veriClaim.claimType === "Offer") { const offerUrl = @@ -879,15 +851,7 @@ export default class ClaimView extends Vue { await this.$logError( "Error getting detailed offer info: " + JSON.stringify(offerResp), ); - this.$notify( - { - group: "alert", - type: "warning", - title: "Error", - text: "Got error retrieving linked offer data.", - }, - 5000, - ); + this.notify.warning("Got error retrieving linked offer data."); } } @@ -911,15 +875,7 @@ export default class ClaimView extends Vue { await this.$logError( "Error retrieving claim: " + JSON.stringify(serverError), ); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "Something went wrong retrieving claim data.", - }, - 3000, - ); + this.notify.error("Something went wrong retrieving claim data.", TIMEOUTS.STANDARD); } } @@ -938,15 +894,7 @@ export default class ClaimView extends Vue { await this.$logError( "Error getting full claim: " + JSON.stringify(resp), ); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "There was a problem getting that claim.", - }, - 5000, - ); + this.notify.error("There was a problem getting that claim."); } } catch (error: unknown) { await this.$logError( @@ -984,31 +932,17 @@ export default class ClaimView extends Vue { " if they can find out more and make an introduction: " + " send them this page and see if they can make a connection for you."; } else { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "Something went wrong retrieving that claim.", - }, - 5000, - ); + this.notify.error("Something went wrong retrieving that claim.", TIMEOUTS.LONG); } } } confirmConfirmClaim() { - this.$notify( - { - group: "modal", - type: "confirm", - title: "Confirm", - text: "Do you personally confirm that this is true?", - onYes: async () => { - await this.confirmClaim(); - }, - }, - -1, + this.notify.confirm( + "Do you personally confirm that this is true?", + async () => { + await this.confirmClaim(); + } ); } @@ -1036,28 +970,12 @@ export default class ClaimView extends Vue { this.axios, ); if (result.success) { - this.$notify( - { - group: "alert", - type: "success", - title: "Success", - text: "Confirmation submitted.", - }, - 5000, - ); + this.notify.confirmationSubmitted(); } else { await this.$logError( "Got error submitting the confirmation: " + JSON.stringify(result), ); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "There was a problem submitting the confirmation.", - }, - 5000, - ); + this.notify.error("There was a problem submitting the confirmation."); } } @@ -1089,15 +1007,7 @@ export default class ClaimView extends Vue { useClipboard() .copy(text) .then(() => { - this.$notify( - { - group: "alert", - type: "toast", - title: "Copied", - text: (name || "That") + " was copied to the clipboard.", - }, - 2000, - ); + this.notify.copied(name || "That"); }); } @@ -1141,15 +1051,7 @@ export default class ClaimView extends Vue { await this.$logError( "Unrecognized claim type for edit: " + this.veriClaim.claimType, ); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "This is an unrecognized claim type.", - }, - 3000, - ); + this.notify.error("This is an unrecognized claim type.", TIMEOUTS.STANDARD); } } } diff --git a/src/views/ShareMyContactInfoView.vue b/src/views/ShareMyContactInfoView.vue index 80fc4d55..391c76f7 100644 --- a/src/views/ShareMyContactInfoView.vue +++ b/src/views/ShareMyContactInfoView.vue @@ -8,12 +8,13 @@
-

-

+
@@ -24,10 +25,12 @@
@@ -43,91 +46,170 @@