diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts index 1ea617be..03eaa153 100644 --- a/src/constants/notifications.ts +++ b/src/constants/notifications.ts @@ -123,6 +123,17 @@ export const NOTIFY_UNCONFIRMED_HOURS = { message: "Would you like to confirm some of those hours?", }; +// Dynamic message template for unconfirmed hours (used in ContactsView.vue) +export const NOTIFY_UNCONFIRMED_HOURS_DYNAMIC = { + title: "Unconfirmed Hours", + // Template: "There {is/are} {count} unconfirmed {hour/hours} from them. Would you like to confirm some of those hours?" + getMessage: (count: number): string => { + const isAre = count === 1 ? "is" : "are"; + const hours = count === 1 ? "hour" : "hours"; + return `There ${isAre} ${count} unconfirmed ${hours} from them. Would you like to confirm some of those hours?`; + }, +}; + // Complex modal constants (for raw $notify calls with advanced features) // MembersList.vue complex modals export const NOTIFY_ADD_CONTACT_FIRST = { @@ -284,3 +295,143 @@ export const NOTIFY_PHOTO_PROCESSING_ERROR = { title: "Processing Error", message: "Failed to process image. Please try again.", }; + +// OnboardMeetingSetupView.vue constants +export const NOTIFY_MEETING_INVALID_TIME = { + title: "Invalid Time", + message: "Select a future time for the meeting expiration.", +}; + +export const NOTIFY_MEETING_NAME_REQUIRED = { + title: "Invalid Name", + message: "Please enter your name.", +}; + +export const NOTIFY_MEETING_PASSWORD_REQUIRED = { + title: "Invalid Password", + message: "Please enter a password.", +}; + +export const NOTIFY_MEETING_CREATED = { + title: "Success", + message: "Meeting created.", +}; + +export const NOTIFY_MEETING_DELETED = { + title: "Success", + message: "Meeting deleted successfully.", +}; + +export const NOTIFY_MEETING_LINK_COPIED = { + title: "Copied", + message: "The member link is copied to the clipboard.", +}; + +// ContactsView.vue extracted notification messages +export const NOTIFY_CONTACT_NO_INFO = { + title: "No Contact Info", + message: "There was no contact info to add. Try the other green buttons.", +}; + +export const NOTIFY_CONTACT_INVALID_URL = { + title: "Invalid URL", + message: "Invalid contact URL format.", +}; + +export const NOTIFY_CONTACTS_ADDED_CSV = { + title: "Contacts Added", + message: "Each contact was added. Nothing was sent to the server.", +}; + +export const NOTIFY_CONTACTS_ADD_ERROR = { + title: "Add Contacts Error", + message: "An error occurred. Some contacts may have been added.", +}; + +export const NOTIFY_CONTACT_INPUT_PARSE_ERROR = { + title: "Invalid Contact List", + message: "The input could not be parsed.", +}; + +export const NOTIFY_CONTACT_NO_CONTACT_FOUND = { + title: "No Contact Info", + message: "No contact info was found in that input.", +}; + +export const NOTIFY_CONTACT_NO_DID = { + title: "Incomplete Contact", + message: "Cannot add a contact without a DID.", +}; + +export const NOTIFY_CONTACT_INVALID_DID = { + title: "Invalid DID", + message: "The DID must begin with 'did:'", +}; + +export const NOTIFY_CONTACT_IMPORT_ERROR = { + title: "Contact Not Added", + message: "An error prevented this import.", +}; + +export const NOTIFY_CONTACT_IMPORT_CONFLICT = { + title: "Contact Not Added", + message: + "A contact with that DID is already in your contact list. Edit them directly below.", +}; + +export const NOTIFY_CONTACT_IMPORT_CONSTRAINT = { + title: "Contact Not Added", + message: "Check that the contact doesn't conflict with any you already have.", +}; + +export const NOTIFY_GIVES_LOAD_ERROR = { + title: "Gives Load Error", + message: "Got an error loading your gives.", +}; + +export const NOTIFY_CONTACT_SETTING_SAVE_ERROR = { + title: "Setting Save Error", + message: + "The setting may not have saved. Try again, maybe after restarting the app.", +}; + +export const NOTIFY_MEETING_STATUS_ERROR = { + title: "Meeting Error", + message: "There was an error checking your meeting status.", +}; + +export const NOTIFY_CONTACTS_ADDED_VISIBLE = { + title: "Contact Added", + message: "They were added, and your activity is visible to them.", +}; + +export const NOTIFY_CONTACTS_ADDED = { + title: "Contact Added", + message: "They were added.", +}; + +export const NOTIFY_CONTACT_INFO_COPY = { + title: "Info", + message: "Contact info will include name, ID, profile image, and public key.", +}; + +export const NOTIFY_CONTACTS_SELECT_TO_COPY = { + title: "Select Contacts", + message: "You must select contacts to copy.", +}; + +export const NOTIFY_CONTACT_LINK_COPIED = { + title: "Copied", + message: "contact link", +}; + +// Template for registration success message +export const getRegisterPersonSuccessMessage = (name?: string): string => + `${name || "That unnamed person"} ${NOTIFY_REGISTER_PERSON_SUCCESS.message}`; + +// Template for visibility success message +export const getVisibilitySuccessMessage = ( + name?: string, + visible = true, +): string => + `${name || "That user"} can ${visible ? "" : "not "}see your activity.`; diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 63757f2c..09cd47bb 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -305,12 +305,31 @@ import { NOTIFY_INVITE_ERROR, NOTIFY_ONBOARDING_CONFIRM, NOTIFY_REGISTER_NOT_AVAILABLE, - NOTIFY_REGISTER_PERSON_SUCCESS, NOTIFY_REGISTER_PERSON_ERROR, NOTIFY_VISIBILITY_ERROR, - NOTIFY_UNCONFIRMED_HOURS, + NOTIFY_UNCONFIRMED_HOURS_DYNAMIC, NOTIFY_REGISTER_CONTACT, NOTIFY_ONBOARDING_MEETING, + NOTIFY_CONTACT_NO_INFO, + NOTIFY_CONTACT_INVALID_URL, + NOTIFY_CONTACTS_ADDED_CSV, + NOTIFY_CONTACTS_ADD_ERROR, + NOTIFY_CONTACT_INPUT_PARSE_ERROR, + NOTIFY_CONTACT_NO_CONTACT_FOUND, + NOTIFY_CONTACT_NO_DID, + NOTIFY_CONTACT_INVALID_DID, + NOTIFY_CONTACT_IMPORT_ERROR, + NOTIFY_CONTACT_IMPORT_CONFLICT, + NOTIFY_CONTACT_IMPORT_CONSTRAINT, + NOTIFY_GIVES_LOAD_ERROR, + NOTIFY_CONTACT_SETTING_SAVE_ERROR, + NOTIFY_MEETING_STATUS_ERROR, + NOTIFY_CONTACTS_ADDED, + NOTIFY_CONTACT_INFO_COPY, + NOTIFY_CONTACTS_SELECT_TO_COPY, + NOTIFY_CONTACT_LINK_COPIED, + getRegisterPersonSuccessMessage, + getVisibilitySuccessMessage, } from "@/constants/notifications"; @Component({ @@ -689,24 +708,24 @@ export default class ContactsView extends Vue { } catch (error) { const fullError = "Error loading gives: " + errorStringForLog(error); this.$logAndConsole(fullError, true); - this.notify.error("Got an error loading your gives.", TIMEOUTS.STANDARD); + this.notify.error(NOTIFY_GIVES_LOAD_ERROR.message, TIMEOUTS.STANDARD); } } private async onClickNewContact(): Promise { const contactInput = this.contactInput.trim(); if (!contactInput) { - this.danger( - "There was no contact info to add. Try the other green buttons.", - "No Contact", - ); + this.notify.error(NOTIFY_CONTACT_NO_INFO.message, TIMEOUTS.STANDARD); return; } if (contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) { const jwt = getContactJwtFromJwtUrl(contactInput); if (!jwt) { - this.danger("Invalid contact URL format.", "Invalid URL"); + this.notify.error( + NOTIFY_CONTACT_INVALID_URL.message, + TIMEOUTS.STANDARD, + ); return; } const { payload } = decodeEndorserJwt(jwt); @@ -729,7 +748,10 @@ export default class ContactsView extends Vue { ) { const jwt = getContactJwtFromJwtUrl(contactInput); if (!jwt) { - this.danger("Invalid contact URL format.", "Invalid URL"); + this.notify.error( + NOTIFY_CONTACT_INVALID_URL.message, + TIMEOUTS.STANDARD, + ); return; } const { payload } = decodeEndorserJwt(jwt); @@ -758,14 +780,17 @@ export default class ContactsView extends Vue { try { await Promise.all(lineAdded); this.notify.success( - "Each contact was added. Nothing was sent to the server.", - TIMEOUTS.STANDARD, // keeping it up so that the "visibility" message is seen + NOTIFY_CONTACTS_ADDED_CSV.message, + TIMEOUTS.STANDARD, ); } catch (e) { const fullError = - "Error adding contacts from CSV: " + errorStringForLog(e); + NOTIFY_CONTACTS_ADD_ERROR.message + ": " + errorStringForLog(e); this.$logAndConsole(fullError, true); - this.danger("An error occurred. Some contacts may have been added."); + this.notify.error( + NOTIFY_CONTACT_INPUT_PARSE_ERROR.message, + TIMEOUTS.STANDARD, + ); } this.contacts = await this.$getAllContacts(); @@ -828,14 +853,19 @@ export default class ContactsView extends Vue { }); } catch (e) { const fullError = - "Error adding contacts from array: " + errorStringForLog(e); + NOTIFY_CONTACT_INPUT_PARSE_ERROR.message + + ": " + + errorStringForLog(e); this.$logAndConsole(fullError, true); - this.danger("The input could not be parsed.", "Invalid Contact List"); + this.notify.error( + NOTIFY_CONTACT_NO_CONTACT_FOUND.message, + TIMEOUTS.STANDARD, + ); } return; } - this.danger("No contact info was found in that input.", "No Contact Info"); + this.notify.error(NOTIFY_CONTACT_NO_INFO.message, TIMEOUTS.STANDARD); } private async addContactFromEndorserMobileLine( @@ -848,11 +878,11 @@ export default class ContactsView extends Vue { private async addContact(newContact: Contact) { if (!newContact.did) { - this.danger("Cannot add a contact without a DID.", "Incomplete Contact"); + this.notify.error(NOTIFY_CONTACT_NO_DID.message, TIMEOUTS.STANDARD); return; } if (!isDid(newContact.did)) { - this.danger("The DID must begin with 'did:'", "Invalid DID"); + this.notify.error(NOTIFY_CONTACT_INVALID_DID.message, TIMEOUTS.STANDARD); return; } @@ -869,10 +899,9 @@ export default class ContactsView extends Vue { if (this.activeDid) { this.setVisibility(newContact, true, false); newContact.seesMe = true; // didn't work inside setVisibility - addedMessage = - "They were added, and your activity is visible to them."; + addedMessage = getVisibilitySuccessMessage(newContact.name || ""); } else { - addedMessage = "They were added."; + addedMessage = NOTIFY_CONTACTS_ADDED.message; } this.contactInput = ""; if (this.isRegistered) { @@ -916,20 +945,22 @@ export default class ContactsView extends Vue { }) .catch((err) => { const fullError = - "Error when adding contact to storage: " + errorStringForLog(err); + NOTIFY_CONTACT_SETTING_SAVE_ERROR.message + + ": " + + errorStringForLog(err); this.$logAndConsole(fullError, true); - let message = "An error prevented this import."; + let message = NOTIFY_CONTACT_IMPORT_ERROR.message; if ( err.message?.indexOf("Key already exists in the object store.") > -1 ) { message = - "A contact with that DID is already in your contact list. Edit them directly below."; + NOTIFY_CONTACT_IMPORT_CONFLICT.message + + ". Check that the contact doesn't conflict with any you already have."; } if (err.name === "ConstraintError") { - message += - " Check that the contact doesn't conflict with any you already have."; + message += NOTIFY_CONTACT_IMPORT_CONSTRAINT.message; } - this.danger(message, "Contact Not Added", 5000); + this.notify.error(message, TIMEOUTS.MODAL); }); } @@ -949,7 +980,9 @@ export default class ContactsView extends Vue { await this.$updateContact(contact.did, { registered: true }); this.notify.success( - `${contact.name || "That unnamed person"} ${NOTIFY_REGISTER_PERSON_SUCCESS.message}`, + getRegisterPersonSuccessMessage( + contact.name || "That unnamed person", + ), TIMEOUTS.STANDARD, ); } else { @@ -1003,7 +1036,7 @@ export default class ContactsView extends Vue { //contact.seesMe = visibility; // why doesn't it affect the UI from here? if (showSuccessAlert) { this.notify.success( - `${contact.name || "That user"} can ${visibility ? "" : "not "}see your activity.`, + getVisibilitySuccessMessage(contact.name || ""), TIMEOUTS.STANDARD, ); } @@ -1026,22 +1059,14 @@ export default class ContactsView extends Vue { recipientDid === this.activeDid && this.givenToMeUnconfirmed[giverDid] > 0 ) { - const isAre = this.givenToMeUnconfirmed[giverDid] == 1 ? "is" : "are"; - const hours = this.givenToMeUnconfirmed[giverDid] == 1 ? "hour" : "hours"; - const message = - "There " + - isAre + - " " + - this.givenToMeUnconfirmed[giverDid] + - " unconfirmed " + - hours + - " from them." + - " Would you like to confirm some of those hours?"; + const message = NOTIFY_UNCONFIRMED_HOURS_DYNAMIC.getMessage( + this.givenToMeUnconfirmed[giverDid], + ); this.$notify( { group: "modal", type: "confirm", - title: NOTIFY_UNCONFIRMED_HOURS.title, + title: NOTIFY_UNCONFIRMED_HOURS_DYNAMIC.title, text: message, onNo: async () => { this.showGiftedDialog(giverDid, recipientDid); @@ -1120,11 +1145,13 @@ export default class ContactsView extends Vue { }); } catch (err) { const fullError = - "Error updating contact-amounts setting: " + errorStringForLog(err); + NOTIFY_CONTACT_SETTING_SAVE_ERROR.message + + ": " + + errorStringForLog(err); this.$logAndConsole(fullError, true); this.notify.error( - "The setting may not have saved. Try again, maybe after restarting the app.", - TIMEOUTS.LONG, + NOTIFY_CONTACT_SETTING_SAVE_ERROR.message, + TIMEOUTS.MODAL, ); } this.showGiveNumbers = newShowValue; @@ -1167,7 +1194,10 @@ export default class ContactsView extends Vue { private async copySelectedContacts() { if (this.contactsSelected.length === 0) { - this.danger("You must select contacts to copy."); + this.notify.error( + NOTIFY_CONTACTS_SELECT_TO_COPY.message, + TIMEOUTS.STANDARD, + ); return; } const selectedContactsFull = this.contacts.filter((c) => @@ -1197,15 +1227,15 @@ export default class ContactsView extends Vue { useClipboard() .copy(contactsJwtUrl) .then(() => { - this.notify.copied("contact link", TIMEOUTS.STANDARD); + this.notify.success( + NOTIFY_CONTACT_LINK_COPIED.message, + TIMEOUTS.STANDARD, + ); }); } private showCopySelectionsInfo() { - this.notify.info( - "Contact info will include name, ID, profile image, and public key.", - TIMEOUTS.LONG, - ); + this.notify.info(NOTIFY_CONTACT_INFO_COPY.message, TIMEOUTS.LONG); } private async showOnboardMeetingDialog() { @@ -1253,13 +1283,10 @@ export default class ContactsView extends Vue { } } catch (error) { this.$logAndConsole( - "Error checking meeting status:" + errorStringForLog(error), + NOTIFY_MEETING_STATUS_ERROR.message + ": " + errorStringForLog(error), true, ); - this.danger( - "There was an error checking your meeting status.", - "Meeting Error", - ); + this.notify.error(NOTIFY_MEETING_STATUS_ERROR.message, TIMEOUTS.MODAL); } } diff --git a/src/views/OnboardMeetingSetupView.vue b/src/views/OnboardMeetingSetupView.vue index bc6c63c7..8ad4cb34 100644 --- a/src/views/OnboardMeetingSetupView.vue +++ b/src/views/OnboardMeetingSetupView.vue @@ -276,16 +276,23 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import QuickNav from "../components/QuickNav.vue"; import TopMessage from "../components/TopMessage.vue"; import MembersList from "../components/MembersList.vue"; -import * as databaseUtil from "../db/databaseUtil"; -import { logConsoleAndDb } from "../db/databaseUtil"; import { errorStringForLog, getHeaders, serverMessageForUser, } from "../libs/endorserServer"; import { encryptMessage } from "../libs/crypto"; -import { logger } from "../utils/logger"; import { APP_SERVER } from "@/constants/app"; +import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; +import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; +import { + NOTIFY_MEETING_INVALID_TIME, + NOTIFY_MEETING_NAME_REQUIRED, + NOTIFY_MEETING_PASSWORD_REQUIRED, + NOTIFY_MEETING_CREATED, + NOTIFY_MEETING_DELETED, + NOTIFY_MEETING_LINK_COPIED, +} from "@/constants/notifications"; interface ServerMeeting { groupId: number; // from the server name: string; // to & from the server @@ -309,6 +316,7 @@ interface MeetingSetupInputs { TopMessage, MembersList, }, + mixins: [PlatformServiceMixin], }) export default class OnboardMeetingView extends Vue { $notify!: ( @@ -317,6 +325,7 @@ export default class OnboardMeetingView extends Vue { ) => void; $route!: RouteLocationNormalizedLoaded; $router!: Router; + notify!: ReturnType; currentMeeting: ServerMeeting | null = null; newOrUpdatedMeetingInputs: MeetingSetupInputs | null = null; @@ -334,11 +343,12 @@ export default class OnboardMeetingView extends Vue { } async created() { - const settings = await databaseUtil.retrieveSettingsForActiveAccount(); - this.activeDid = settings.activeDid || ""; - this.apiServer = settings.apiServer || ""; - this.fullName = settings.firstName || ""; - this.isRegistered = !!settings.isRegistered; + this.notify = createNotifyHelpers(this.$notify as any); + const settings = await this.$getSettings("activeAccount"); + this.activeDid = settings?.activeDid || ""; + this.apiServer = settings?.apiServer || ""; + this.fullName = settings?.firstName || ""; + this.isRegistered = !!settings?.isRegistered; await this.fetchCurrentMeeting(); this.isLoading = false; @@ -426,38 +436,20 @@ export default class OnboardMeetingView extends Vue { const localExpiresAt = new Date(this.newOrUpdatedMeetingInputs.expiresAt); const now = new Date(); if (localExpiresAt <= now) { - this.$notify( - { - group: "alert", - type: "warning", - title: "Invalid Time", - text: "Select a future time for the meeting expiration.", - }, - 5000, - ); + this.notify.warning(NOTIFY_MEETING_INVALID_TIME.message, TIMEOUTS.LONG); return; } if (!this.newOrUpdatedMeetingInputs.userFullName) { - this.$notify( - { - group: "alert", - type: "warning", - title: "Invalid Name", - text: "Please enter your name.", - }, - 5000, + this.notify.warning( + NOTIFY_MEETING_NAME_REQUIRED.message, + TIMEOUTS.LONG, ); return; } if (!this.newOrUpdatedMeetingInputs.password) { - this.$notify( - { - group: "alert", - type: "warning", - title: "Invalid Password", - text: "Please enter a password.", - }, - 5000, + this.notify.warning( + NOTIFY_MEETING_PASSWORD_REQUIRED.message, + TIMEOUTS.LONG, ); return; } @@ -492,33 +484,19 @@ export default class OnboardMeetingView extends Vue { }; this.newOrUpdatedMeetingInputs = null; - this.$notify( - { - group: "alert", - type: "success", - title: "Success", - text: "Meeting created.", - }, - 3000, - ); + this.notify.success(NOTIFY_MEETING_CREATED.message, TIMEOUTS.STANDARD); } else { throw { response: response }; } } catch (error) { - logConsoleAndDb( + this.$logAndConsole( "Error creating meeting: " + errorStringForLog(error), true, ); const errorMessage = serverMessageForUser(error); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: - errorMessage || - "Failed to create meeting. Try reloading or submitting again.", - }, + this.notify.error( + errorMessage || + "Failed to create meeting. Try reloading or submitting again.", 5000, ); } finally { @@ -560,25 +538,12 @@ export default class OnboardMeetingView extends Vue { this.newOrUpdatedMeetingInputs = this.blankMeeting(); this.showDeleteConfirm = false; - this.$notify( - { - group: "alert", - type: "success", - title: "Success", - text: "Meeting deleted successfully.", - }, - 3000, - ); + this.notify.success(NOTIFY_MEETING_DELETED.message, TIMEOUTS.STANDARD); } catch (error) { - logger.error("Error deleting meeting:", error); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: serverMessageForUser(error) || "Failed to delete meeting.", - }, - 5000, + this.$logError("Error deleting meeting: " + error); + this.notify.error( + serverMessageForUser(error) || "Failed to delete meeting.", + TIMEOUTS.LONG, ); } finally { this.isDeleting = false; @@ -597,7 +562,7 @@ export default class OnboardMeetingView extends Vue { projectLink: this.currentMeeting.projectLink || "", }; } else { - logger.error( + this.$logError( "There is no current meeting to edit. We should never get here.", ); } @@ -619,38 +584,20 @@ export default class OnboardMeetingView extends Vue { const localExpiresAt = new Date(this.newOrUpdatedMeetingInputs.expiresAt); const now = new Date(); if (localExpiresAt <= now) { - this.$notify( - { - group: "alert", - type: "warning", - title: "Invalid Time", - text: "Select a future time for the meeting expiration.", - }, - 5000, - ); + this.notify.warning(NOTIFY_MEETING_INVALID_TIME.message, TIMEOUTS.LONG); return; } if (!this.newOrUpdatedMeetingInputs.userFullName) { - this.$notify( - { - group: "alert", - type: "warning", - title: "Invalid Name", - text: "Please enter your name.", - }, - 5000, + this.notify.warning( + NOTIFY_MEETING_NAME_REQUIRED.message, + TIMEOUTS.LONG, ); return; } if (!this.newOrUpdatedMeetingInputs.password) { - this.$notify( - { - group: "alert", - type: "warning", - title: "Invalid Password", - text: "Please enter a password.", - }, - 5000, + this.notify.warning( + NOTIFY_MEETING_PASSWORD_REQUIRED.message, + TIMEOUTS.LONG, ); return; } @@ -696,21 +643,15 @@ export default class OnboardMeetingView extends Vue { throw { response: response }; } } catch (error) { - logConsoleAndDb( + this.$logAndConsole( "Error updating meeting: " + errorStringForLog(error), true, ); const errorMessage = serverMessageForUser(error); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: - errorMessage || - "Failed to update meeting. Try reloading or submitting again.", - }, - 5000, + this.notify.error( + errorMessage || + "Failed to update meeting. Try reloading or submitting again.", + TIMEOUTS.LONG, ); } finally { this.isLoading = false; @@ -727,30 +668,14 @@ export default class OnboardMeetingView extends Vue { } handleMembersError(message: string) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: message, - }, - 5000, - ); + this.notify.error(message, TIMEOUTS.LONG); } copyMembersLinkToClipboard() { useClipboard() .copy(this.onboardMeetingMembersLink()) .then(() => { - this.$notify( - { - group: "alert", - type: "info", - title: "Copied", - text: "The member link is copied to the clipboard.", - }, - 5000, - ); + this.notify.info(NOTIFY_MEETING_LINK_COPIED.message, TIMEOUTS.LONG); }); } }