@@ -243,7 +186,7 @@
Before you can upload a photo, a friend needs to register you.
Share Your Info
@@ -262,10 +205,20 @@ import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { Capacitor } from "@capacitor/core";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
+import {
+ NOTIFY_IMAGE_DIALOG_SETTINGS_ERROR,
+ NOTIFY_IMAGE_DIALOG_RETRIEVAL_ERROR,
+ NOTIFY_IMAGE_DIALOG_CAPTURE_ERROR,
+ NOTIFY_IMAGE_DIALOG_NO_IMAGE_ERROR,
+ createImageDialogCameraErrorMessage,
+ createImageDialogUploadErrorMessage,
+ IMAGE_DIALOG_TIMEOUT_LONG,
+ IMAGE_DIALOG_TIMEOUT_MODAL,
+} from "../constants/notifications";
import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
-import * as databaseUtil from "../db/databaseUtil";
+import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { Prop } from "vue-facing-decorator";
import { Router } from "vue-router";
@@ -273,6 +226,7 @@ const inputImageFileNameRef = ref();
@Component({
components: { VuePictureCropper },
+ mixins: [PlatformServiceMixin],
})
export default class ImageMethodDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -317,7 +271,7 @@ export default class ImageMethodDialog extends Vue {
/** Current camera facing mode */
private currentFacingMode: "environment" | "user" = "environment";
- private platformService = PlatformServiceFactory.getInstance();
+ platformService = PlatformServiceFactory.getInstance();
URL = window.URL || window.webkitURL;
private platformCapabilities = this.platformService.getCapabilities();
@@ -350,13 +304,233 @@ export default class ImageMethodDialog extends Vue {
})
defaultCameraMode!: string;
+ /**
+ * Computed property for dialog heading text
+ * Determines the appropriate heading based on current state
+ */
+ get dialogHeading(): string {
+ if (this.uploading) return "Uploading Imageā¦";
+ if (this.blob) return this.crop ? "Crop Image" : "Preview Image";
+ if (this.showCameraPreview) return "Upload Image";
+ return "Add Photo";
+ }
+
+ /**
+ * Computed property for camera preview visibility
+ * Determines if camera preview should be shown
+ */
+ get shouldShowCameraPreview(): boolean {
+ return this.showCameraPreview && !this.blob;
+ }
+
+ /**
+ * Computed property for camera feedback visibility
+ * Shows feedback when camera preview is not visible after mounting
+ */
+ get shouldShowCameraFeedback(): boolean {
+ return !this.showCameraPreview && !this.blob && this.isRegistered;
+ }
+
+ /**
+ * Computed property for camera feedback message
+ * Provides appropriate feedback based on camera state
+ */
+ get cameraFeedbackMessage(): string {
+ if (this.cameraState === "off") {
+ if (this.platformCapabilities.isMobile) {
+ return "This mobile browser may not support direct camera access, or the app is treating it as a native app. Try using a desktop browser, or check if your browser supports camera access for web apps.";
+ }
+ return "Your browser supports camera APIs, but the preview did not start. Try refreshing the page or checking browser permissions.";
+ }
+ if (this.cameraState === "error") {
+ return this.error || this.cameraStateMessage || "Unknown error occurred.";
+ }
+ return this.cameraStateMessage || "Unknown reason.";
+ }
+
+ /**
+ * Computed property for button grid classes
+ * Determines grid layout based on retry button visibility
+ */
+ get buttonGridClasses(): string {
+ return `grid gap-2 mt-2 ${this.showRetry ? "grid-cols-2" : "grid-cols-1"}`;
+ }
+
+ /**
+ * Computed property for blob URL
+ * Creates object URL for blob display
+ */
+ get blobUrl(): string {
+ return this.blob ? this.createBlobURL(this.blob) : "";
+ }
+
+ /**
+ * Computed property for diagnostics toggle button text
+ * Determines button text based on diagnostics visibility
+ */
+ get diagnosticsToggleText(): string {
+ return this.showDiagnostics ? "Hide Diagnostics" : "Show Diagnostics";
+ }
+
+ /**
+ * Computed property for camera rotation button visibility
+ * Shows rotation button only on mobile platforms
+ */
+ get shouldShowCameraRotation(): boolean {
+ return this.platformCapabilities.isMobile;
+ }
+
+ /**
+ * Computed property for close button classes
+ * Provides consistent styling for the close button
+ */
+ get closeButtonClasses(): string {
+ return "text-2xl text-center px-1 py-0.5 leading-none absolute -right-1 top-0";
+ }
+
+ /**
+ * Computed property for camera feedback container classes
+ * Provides consistent styling for camera feedback messages
+ */
+ get cameraFeedbackClasses(): string {
+ return "bg-red-100 text-red-700 border border-red-400 rounded px-4 py-3 my-4 text-sm";
+ }
+
+ /**
+ * Computed property for section divider classes
+ * Provides consistent styling for section dividers
+ */
+ get sectionDividerClasses(): string {
+ return "border-b border-dashed border-slate-300 text-orange-400 mb-4 font-bold text-sm";
+ }
+
+ /**
+ * Computed property for section divider span classes
+ * Provides consistent styling for divider labels
+ */
+ get sectionDividerSpanClasses(): string {
+ return "block w-fit mx-auto -mb-2.5 bg-white px-2";
+ }
+
+ /**
+ * Computed property for camera preview container classes
+ * Provides consistent styling for camera preview
+ */
+ get cameraPreviewClasses(): string {
+ return "camera-preview relative flex bg-black overflow-hidden mb-4";
+ }
+
+ /**
+ * Computed property for diagnostics panel classes
+ * Provides consistent styling for diagnostics overlay
+ */
+ get diagnosticsPanelClasses(): string {
+ return "absolute top-0 left-0 right-0 bg-black/80 text-white text-xs p-2 pt-8 z-20 overflow-auto max-h-[50vh]";
+ }
+
+ /**
+ * Computed property for diagnostics toggle button classes
+ * Provides consistent styling for diagnostics toggle
+ */
+ get diagnosticsToggleClasses(): string {
+ return "absolute top-2 right-2 bg-black/50 text-white px-2 py-1 rounded text-xs z-30";
+ }
+
+ /**
+ * Computed property for camera control button classes
+ * Provides consistent styling for camera control buttons
+ */
+ get cameraControlButtonClasses(): string {
+ return "bg-white text-slate-800 p-3 rounded-full text-2xl leading-none";
+ }
+
+ /**
+ * Computed property for camera controls container classes
+ * Provides consistent styling for camera controls
+ */
+ get cameraControlsClasses(): string {
+ return "absolute bottom-4 inset-x-0 flex items-center justify-center gap-4";
+ }
+
+ /**
+ * Computed property for file input classes
+ * Provides consistent styling for file input with custom button
+ */
+ get fileInputClasses(): string {
+ return "w-full file:text-center file:bg-gradient-to-b file:from-slate-400 file:to-slate-700 file:shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] file:text-white file:px-3 file:py-2 file:rounded-md file:border-none file:cursor-pointer file:me-2";
+ }
+
+ /**
+ * Computed property for URL input classes
+ * Provides consistent styling for URL input field
+ */
+ get urlInputClasses(): string {
+ return "block w-full rounded border border-slate-400 px-4 py-2";
+ }
+
+ /**
+ * Computed property for accept URL button classes
+ * Provides consistent styling for accept URL button
+ */
+ get acceptUrlButtonClasses(): string {
+ return "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-3 py-2 rounded-md cursor-pointer";
+ }
+
+ /**
+ * Computed property for image container classes
+ * Provides consistent styling for image display
+ */
+ get imageContainerClasses(): string {
+ return "mt-2 rounded max-h-[50vh] max-w-[90vw] object-contain";
+ }
+
+ /**
+ * Computed property for cropper container classes
+ * Provides consistent styling for image cropper
+ */
+ get cropperClasses(): string {
+ return "max-h-[50vh] max-w-[90vw] object-contain";
+ }
+
+ /**
+ * Computed property for primary button classes
+ * Provides consistent styling for primary action buttons
+ */
+ get primaryButtonClasses(): string {
+ return "bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md";
+ }
+
+ /**
+ * Computed property for secondary button classes
+ * Provides consistent styling for secondary action buttons
+ */
+ get secondaryButtonClasses(): string {
+ return "bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md";
+ }
+
+ /**
+ * Computed property for registration notice classes
+ * Provides consistent styling for registration notice
+ */
+ get registrationNoticeClasses(): string {
+ return "bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3";
+ }
+
+ /**
+ * Computed property for registration button classes
+ * Provides consistent styling for registration button
+ */
+ get registrationButtonClasses(): string {
+ return "inline-block text-md uppercase 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-4 py-2 rounded-md";
+ }
+
/**
* Lifecycle hook: Initializes component and retrieves user settings
* @throws {Error} When settings retrieval fails
*/
async mounted() {
try {
- const settings = await databaseUtil.retrieveSettingsForActiveAccount();
+ const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
} catch (error: unknown) {
logger.error("Error retrieving settings from database:", error);
@@ -368,9 +542,9 @@ export default class ImageMethodDialog extends Vue {
text:
error instanceof Error
? error.message
- : "There was an error retrieving your settings.",
+ : NOTIFY_IMAGE_DIALOG_SETTINGS_ERROR.message,
},
- -1,
+ IMAGE_DIALOG_TIMEOUT_MODAL,
);
}
}
@@ -435,9 +609,9 @@ export default class ImageMethodDialog extends Vue {
group: "alert",
type: "danger",
title: "Error",
- text: "There was an error retrieving that image.",
+ text: NOTIFY_IMAGE_DIALOG_RETRIEVAL_ERROR.message,
},
- 5000,
+ IMAGE_DIALOG_TIMEOUT_LONG,
);
}
} else {
@@ -510,22 +684,9 @@ export default class ImageMethodDialog extends Vue {
}
} catch (error) {
logger.error("Error starting camera preview:", error);
- let errorMessage =
- error instanceof Error ? error.message : "Failed to access camera";
- if (
- error instanceof Error &&
- (error.name === "NotReadableError" || error.name === "TrackStartError")
- ) {
- errorMessage =
- "Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
- } else if (
- error instanceof Error &&
- (error.name === "NotAllowedError" ||
- error.name === "PermissionDeniedError")
- ) {
- errorMessage =
- "Camera access was denied. Please allow camera access in your browser settings.";
- }
+ const errorMessage = createImageDialogCameraErrorMessage(
+ error instanceof Error ? error : new Error("Unknown camera error"),
+ );
this.cameraState = "error";
this.cameraStateMessage = errorMessage;
this.error = errorMessage;
@@ -537,7 +698,7 @@ export default class ImageMethodDialog extends Vue {
title: "Error",
text: errorMessage,
},
- 5000,
+ IMAGE_DIALOG_TIMEOUT_LONG,
);
}
}
@@ -583,9 +744,9 @@ export default class ImageMethodDialog extends Vue {
group: "alert",
type: "danger",
title: "Error",
- text: "Failed to capture photo. Please try again.",
+ text: NOTIFY_IMAGE_DIALOG_CAPTURE_ERROR.message,
},
- 5000,
+ IMAGE_DIALOG_TIMEOUT_LONG,
);
}
}
@@ -634,9 +795,9 @@ export default class ImageMethodDialog extends Vue {
group: "alert",
type: "danger",
title: "Error",
- text: "There was an error finding the picture. Please try again.",
+ text: NOTIFY_IMAGE_DIALOG_NO_IMAGE_ERROR.message,
},
- 5000,
+ IMAGE_DIALOG_TIMEOUT_LONG,
);
this.uploading = false;
this.close();
@@ -663,25 +824,7 @@ export default class ImageMethodDialog extends Vue {
this.close();
this.imageCallback(response.data.url as string);
} catch (error: unknown) {
- let errorMessage = "There was an error saving the picture.";
-
- if (axios.isAxiosError(error)) {
- const status = error.response?.status;
- const data = error.response?.data;
-
- if (status === 401) {
- errorMessage = "Authentication failed. Please try logging in again.";
- } else if (status === 413) {
- errorMessage = "Image file is too large. Please try a smaller image.";
- } else if (status === 415) {
- errorMessage =
- "Unsupported image format. Please try a different image.";
- } else if (status && status >= 500) {
- errorMessage = "Server error. Please try again later.";
- } else if (data?.message) {
- errorMessage = data.message;
- }
- }
+ const errorMessage = createImageDialogUploadErrorMessage(error);
this.$notify(
{
@@ -690,7 +833,7 @@ export default class ImageMethodDialog extends Vue {
title: "Error",
text: errorMessage,
},
- 5000,
+ IMAGE_DIALOG_TIMEOUT_LONG,
);
this.uploading = false;
this.blob = undefined;
diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts
index b4ecb6ab..233d21ad 100644
--- a/src/constants/notifications.ts
+++ b/src/constants/notifications.ts
@@ -1,3 +1,5 @@
+import axios from "axios";
+
// Notification message constants for user-facing notifications
// Add new notification messages here as needed
//
@@ -1382,3 +1384,235 @@ export const QR_TIMEOUT_SHORT = 1000; // Short operations like registration subm
export const QR_TIMEOUT_MEDIUM = 2000; // Medium operations like URL copy
export const QR_TIMEOUT_STANDARD = 3000; // Standard success messages
export const QR_TIMEOUT_LONG = 5000; // Error messages and warnings
+
+// NewEditProjectView.vue specific constants
+// Used in: NewEditProjectView.vue (mounted method - account loading error)
+export const NOTIFY_PROJECT_ACCOUNT_LOADING_ERROR = {
+ title: "Account Loading Error",
+ message: "There was a problem loading your account info.",
+};
+
+// Used in: NewEditProjectView.vue (loadProject method - project retrieval error)
+export const NOTIFY_PROJECT_RETRIEVAL_ERROR = {
+ title: "Project Retrieval Error",
+ message: "There was an error retrieving that project.",
+};
+
+// Used in: NewEditProjectView.vue (confirmDeleteImage method - image deletion confirmation)
+export const NOTIFY_PROJECT_DELETE_IMAGE_CONFIRM = {
+ title: "Delete Image",
+ message: "Are you sure you want to delete the image?",
+};
+
+// Used in: NewEditProjectView.vue (deleteImage method - image deletion error)
+export const NOTIFY_PROJECT_DELETE_IMAGE_ERROR = {
+ title: "Image Deletion Error",
+ message: "There was a problem deleting the image.",
+};
+
+// Used in: NewEditProjectView.vue (deleteImage method - image deletion general error)
+export const NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR = {
+ title: "Image Deletion Error",
+ message: "There was an error deleting the image.",
+};
+
+// Used in: NewEditProjectView.vue (validateLocation method - invalid location error)
+export const NOTIFY_PROJECT_INVALID_LOCATION = {
+ title: "Invalid Location",
+ message: "The location was invalid so it was not set.",
+};
+
+// Used in: NewEditProjectView.vue (validateStartDate method - invalid start date error)
+export const NOTIFY_PROJECT_INVALID_START_DATE = {
+ title: "Invalid Start Date",
+ message: "The start date was invalid so it was not set.",
+};
+
+// Used in: NewEditProjectView.vue (validateEndDate method - invalid end date error)
+export const NOTIFY_PROJECT_INVALID_END_DATE = {
+ title: "Invalid End Date",
+ message: "The end date was invalid so it was not set.",
+};
+
+// Used in: NewEditProjectView.vue (saveProject method - project save success)
+export const NOTIFY_PROJECT_SAVE_SUCCESS = {
+ title: "Success",
+ message: "The project was saved successfully.",
+};
+
+// Used in: NewEditProjectView.vue (saveProject method - partner location warning)
+export const NOTIFY_PROJECT_PARTNER_LOCATION_WARNING = {
+ title: "Partner Location Warning",
+ message:
+ "A partner was selected but the location was not set, so it was not sent to any partner.",
+};
+
+// Used in: NewEditProjectView.vue (sendToNostrPartner method - partner send success)
+export const NOTIFY_PROJECT_PARTNER_SEND_SUCCESS = {
+ title: "Partner Integration Success",
+ message: "The project info was sent to",
+};
+
+// Used in: NewEditProjectView.vue (sendToNostrPartner method - partner send error)
+export const NOTIFY_PROJECT_PARTNER_SEND_ERROR = {
+ title: "Partner Integration Error",
+ message: "Failed sending to",
+};
+
+// Used in: NewEditProjectView.vue (sendToNostrPartner method - partner send general error)
+export const NOTIFY_PROJECT_PARTNER_SEND_GENERAL_ERROR = {
+ title: "Partner Integration Error",
+ message: "There was an error sending to the partner service.",
+};
+
+// Used in: NewEditProjectView.vue (confirmEraseLatLong method - location deletion confirmation)
+export const NOTIFY_PROJECT_DELETE_LOCATION_CONFIRM = {
+ title: "Delete Location",
+ message: "Are you sure you want to delete the location?",
+};
+
+// Used in: NewEditProjectView.vue (showNostrPartnerInfo method - partner info)
+export const NOTIFY_PROJECT_NOSTR_PARTNER_INFO = {
+ title: "Partner Integration Info",
+ message:
+ "This will share your project information with external partner services using Nostr protocol.",
+};
+
+// Helper function for dynamic partner send success messages
+// Used in: NewEditProjectView.vue (sendToNostrPartner method - dynamic success message)
+export function createProjectPartnerSendSuccessMessage(
+ serviceName: string,
+): string {
+ return `${NOTIFY_PROJECT_PARTNER_SEND_SUCCESS.message} ${serviceName}.`;
+}
+
+// Helper function for dynamic partner send error messages
+// Used in: NewEditProjectView.vue (sendToNostrPartner method - dynamic error message)
+export function createProjectPartnerSendErrorMessage(
+ serviceName: string,
+ errorData: string,
+): string {
+ return `${NOTIFY_PROJECT_PARTNER_SEND_ERROR.message} ${serviceName}: ${errorData}`;
+}
+
+// NewEditProjectView.vue timeout constants
+export const PROJECT_TIMEOUT_SHORT = 1000; // Short operations like confirmations
+export const PROJECT_TIMEOUT_STANDARD = 3000; // Standard success messages
+export const PROJECT_TIMEOUT_LONG = 5000; // Error messages and warnings
+export const PROJECT_TIMEOUT_VERY_LONG = 7000; // Complex operations and partner errors
+
+// ImageMethodDialog.vue specific constants
+// Used in: ImageMethodDialog.vue (mounted method - settings retrieval error)
+export const NOTIFY_IMAGE_DIALOG_SETTINGS_ERROR = {
+ title: "Error",
+ message: "There was an error retrieving your settings.",
+};
+
+// Used in: ImageMethodDialog.vue (acceptUrl method - image retrieval error)
+export const NOTIFY_IMAGE_DIALOG_RETRIEVAL_ERROR = {
+ title: "Error",
+ message: "There was an error retrieving that image.",
+};
+
+// Used in: ImageMethodDialog.vue (startCameraPreview method - camera access error)
+export const NOTIFY_IMAGE_DIALOG_CAMERA_ACCESS_ERROR = {
+ title: "Error",
+ message: "Failed to access camera",
+};
+
+// Used in: ImageMethodDialog.vue (startCameraPreview method - camera in use error)
+export const NOTIFY_IMAGE_DIALOG_CAMERA_IN_USE = {
+ title: "Camera in Use",
+ message:
+ "Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.",
+};
+
+// Used in: ImageMethodDialog.vue (startCameraPreview method - camera permission denied)
+export const NOTIFY_IMAGE_DIALOG_CAMERA_PERMISSION_DENIED = {
+ title: "Camera Access Denied",
+ message:
+ "Camera access was denied. Please allow camera access in your browser settings.",
+};
+
+// Used in: ImageMethodDialog.vue (capturePhoto method - photo capture error)
+export const NOTIFY_IMAGE_DIALOG_CAPTURE_ERROR = {
+ title: "Error",
+ message: "Failed to capture photo. Please try again.",
+};
+
+// Used in: ImageMethodDialog.vue (uploadImage method - no image found error)
+export const NOTIFY_IMAGE_DIALOG_NO_IMAGE_ERROR = {
+ title: "Error",
+ message: "There was an error finding the picture. Please try again.",
+};
+
+// Used in: ImageMethodDialog.vue (uploadImage method - general upload error)
+export const NOTIFY_IMAGE_DIALOG_UPLOAD_ERROR = {
+ title: "Error",
+ message: "There was an error saving the picture.",
+};
+
+// Used in: ImageMethodDialog.vue (uploadImage method - authentication error)
+export const NOTIFY_IMAGE_DIALOG_AUTH_ERROR = {
+ title: "Authentication Error",
+ message: "Authentication failed. Please try logging in again.",
+};
+
+// Used in: ImageMethodDialog.vue (uploadImage method - file too large error)
+export const NOTIFY_IMAGE_DIALOG_FILE_TOO_LARGE = {
+ title: "File Too Large",
+ message: "Image file is too large. Please try a smaller image.",
+};
+
+// Used in: ImageMethodDialog.vue (uploadImage method - unsupported format error)
+export const NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT = {
+ title: "Unsupported Format",
+ message: "Unsupported image format. Please try a different image.",
+};
+
+// Used in: ImageMethodDialog.vue (uploadImage method - server error)
+export const NOTIFY_IMAGE_DIALOG_SERVER_ERROR = {
+ title: "Server Error",
+ message: "Server error. Please try again later.",
+};
+
+// Helper function for dynamic camera error messages
+// Used in: ImageMethodDialog.vue (startCameraPreview method - dynamic camera error message)
+export function createImageDialogCameraErrorMessage(error: Error): string {
+ if (error.name === "NotReadableError" || error.name === "TrackStartError") {
+ return NOTIFY_IMAGE_DIALOG_CAMERA_IN_USE.message;
+ } else if (
+ error.name === "NotAllowedError" ||
+ error.name === "PermissionDeniedError"
+ ) {
+ return NOTIFY_IMAGE_DIALOG_CAMERA_PERMISSION_DENIED.message;
+ }
+ return error.message || NOTIFY_IMAGE_DIALOG_CAMERA_ACCESS_ERROR.message;
+}
+
+// Helper function for dynamic upload error messages
+// Used in: ImageMethodDialog.vue (uploadImage method - dynamic upload error message)
+export function createImageDialogUploadErrorMessage(error: any): string {
+ if (axios.isAxiosError(error)) {
+ const status = error.response?.status;
+ const data = error.response?.data;
+
+ if (status === 401) {
+ return NOTIFY_IMAGE_DIALOG_AUTH_ERROR.message;
+ } else if (status === 413) {
+ return NOTIFY_IMAGE_DIALOG_FILE_TOO_LARGE.message;
+ } else if (status === 415) {
+ return NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT.message;
+ } else if (status && status >= 500) {
+ return NOTIFY_IMAGE_DIALOG_SERVER_ERROR.message;
+ } else if (data?.message) {
+ return data.message;
+ }
+ }
+ return NOTIFY_IMAGE_DIALOG_UPLOAD_ERROR.message;
+}
+
+// ImageMethodDialog.vue timeout constants
+export const IMAGE_DIALOG_TIMEOUT_STANDARD = 3000; // Standard error messages
+export const IMAGE_DIALOG_TIMEOUT_LONG = 5000; // Camera and upload errors
+export const IMAGE_DIALOG_TIMEOUT_MODAL = -1; // Modal confirmations (no auto-dismiss)
diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue
index 25dec581..c39a9d42 100644
--- a/src/views/NewEditProjectView.vue
+++ b/src/views/NewEditProjectView.vue
@@ -235,7 +235,25 @@ import {
NotificationIface,
} from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
-import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
+import { createNotifyHelpers } from "../utils/notify";
+import {
+ NOTIFY_PROJECT_ACCOUNT_LOADING_ERROR,
+ NOTIFY_PROJECT_RETRIEVAL_ERROR,
+ NOTIFY_PROJECT_DELETE_IMAGE_CONFIRM,
+ NOTIFY_PROJECT_DELETE_IMAGE_ERROR,
+ NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR,
+ NOTIFY_PROJECT_INVALID_LOCATION,
+ NOTIFY_PROJECT_INVALID_START_DATE,
+ NOTIFY_PROJECT_INVALID_END_DATE,
+ NOTIFY_PROJECT_SAVE_SUCCESS,
+ NOTIFY_PROJECT_PARTNER_LOCATION_WARNING,
+ NOTIFY_PROJECT_PARTNER_SEND_GENERAL_ERROR,
+ NOTIFY_PROJECT_DELETE_LOCATION_CONFIRM,
+ NOTIFY_PROJECT_NOSTR_PARTNER_INFO,
+ createProjectPartnerSendSuccessMessage,
+ createProjectPartnerSendErrorMessage,
+ PROJECT_TIMEOUT_VERY_LONG,
+} from "../constants/notifications";
import { PlanActionClaim } from "../interfaces/claims";
import {
createEndorserJwtVcFromClaim,
@@ -310,7 +328,7 @@ export default class NewEditProjectView extends Vue {
$router!: Router;
// Notification helpers
- private notifyHelpers = createNotifyHelpers(this.$notify);
+ private notify!: ReturnType;
/**
* Display error notification to user
@@ -318,7 +336,7 @@ export default class NewEditProjectView extends Vue {
* @param message - Error message to display
*/
errNote(message: string) {
- this.notifyHelpers.error(message);
+ this.notify.error(message);
}
// Component state properties
@@ -358,6 +376,9 @@ export default class NewEditProjectView extends Vue {
* Handles account validation and project loading with comprehensive error handling
*/
async mounted() {
+ // Initialize notification helpers
+ this.notify = createNotifyHelpers(this.$notify);
+
this.numAccounts = await retrieveAccountCount();
const settings = await this.$accountSettings();
@@ -369,9 +390,7 @@ export default class NewEditProjectView extends Vue {
if (this.projectId) {
if (this.numAccounts === 0) {
- this.notifyHelpers.error(
- "There was a problem loading your account info.",
- );
+ this.notify.error(NOTIFY_PROJECT_ACCOUNT_LOADING_ERROR.message);
} else {
this.loadProject(this.activeDid);
}
@@ -422,7 +441,7 @@ export default class NewEditProjectView extends Vue {
}
} catch (error) {
logger.error("Got error retrieving that project", error);
- this.notifyHelpers.error("There was an error retrieving that project.");
+ this.notify.error(NOTIFY_PROJECT_RETRIEVAL_ERROR.message);
}
}
@@ -441,8 +460,8 @@ export default class NewEditProjectView extends Vue {
* Shows confirmation dialog before proceeding with image deletion
*/
confirmDeleteImage() {
- this.notifyHelpers.confirm(
- "Are you sure you want to delete the image?",
+ this.notify.confirm(
+ NOTIFY_PROJECT_DELETE_IMAGE_CONFIRM.message,
async () => {
await this.deleteImage();
},
@@ -480,7 +499,7 @@ export default class NewEditProjectView extends Vue {
// (either they'll simply continue or they're canceling and going back)
} else {
logger.error("Problem deleting image:", response);
- this.notifyHelpers.error("There was a problem deleting the image.");
+ this.notify.error(NOTIFY_PROJECT_DELETE_IMAGE_ERROR.message);
return;
}
@@ -495,7 +514,7 @@ export default class NewEditProjectView extends Vue {
// it already doesn't exist so we won't say anything to the user
} else {
- this.notifyHelpers.error("There was an error deleting the image.");
+ this.notify.error(NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR.message);
}
}
}
@@ -525,7 +544,7 @@ export default class NewEditProjectView extends Vue {
}
if (this.includeLocation) {
if (!this.latitude || !this.longitude) {
- this.notifyHelpers.error("The location was invalid so it was not set.");
+ this.notify.error(NOTIFY_PROJECT_INVALID_LOCATION.message);
delete vcClaim.location;
} else {
vcClaim.location = {
@@ -548,9 +567,7 @@ export default class NewEditProjectView extends Vue {
} catch {
// it's not a valid date so erase it and tell the user
delete vcClaim.startTime;
- this.notifyHelpers.error(
- "The start date was invalid so it was not set.",
- );
+ this.notify.error(NOTIFY_PROJECT_INVALID_START_DATE.message);
}
} else {
delete vcClaim.startTime;
@@ -564,7 +581,7 @@ export default class NewEditProjectView extends Vue {
} catch {
// it's not a valid date so erase it and tell the user
delete vcClaim.endTime;
- this.notifyHelpers.error("The end date was invalid so it was not set.");
+ this.notify.error(NOTIFY_PROJECT_INVALID_END_DATE.message);
}
} else {
delete vcClaim.endTime;
@@ -580,7 +597,7 @@ export default class NewEditProjectView extends Vue {
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success?.handleId) {
- this.notifyHelpers.success("The project was saved successfully.");
+ this.notify.success(NOTIFY_PROJECT_SAVE_SUCCESS.message);
this.errorMessage = "";
@@ -614,9 +631,7 @@ export default class NewEditProjectView extends Vue {
);
}
} else {
- this.notifyHelpers.error(
- "A partner was selected but the location was not set, so it was not sent to any partner.",
- );
+ this.notify.error(NOTIFY_PROJECT_PARTNER_LOCATION_WARNING.message);
}
}
@@ -654,7 +669,7 @@ export default class NewEditProjectView extends Vue {
}
}
if (userMessage) {
- this.notifyHelpers.error(userMessage);
+ this.notify.error(userMessage);
}
// Now set that error for the user to see.
this.errorMessage = userMessage;
@@ -756,23 +771,26 @@ export default class NewEditProjectView extends Vue {
{ headers },
);
if (linkResp.status === 201) {
- this.notifyHelpers.success(
- `The project info was sent to ${serviceName}.`,
+ this.notify.success(
+ createProjectPartnerSendSuccessMessage(serviceName),
);
} else {
// axios never gets here because it throws an error, but just in case
- this.notifyHelpers.error(
- `Failed sending to ${serviceName}: ${JSON.stringify(linkResp.data)}`,
+ this.notify.error(
+ createProjectPartnerSendErrorMessage(
+ serviceName,
+ JSON.stringify(linkResp.data),
+ ),
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
logger.error(`Error sending to ${serviceName}`, error);
- let errorMessage = `There was an error sending to ${serviceName}.`;
+ let errorMessage = NOTIFY_PROJECT_PARTNER_SEND_GENERAL_ERROR.message;
if (error.response?.data?.error?.message) {
errorMessage = error.response.data.error.message;
}
- this.notifyHelpers.error(errorMessage, TIMEOUTS.VERY_LONG);
+ this.notify.error(errorMessage, PROJECT_TIMEOUT_VERY_LONG);
}
}
@@ -796,8 +814,8 @@ export default class NewEditProjectView extends Vue {
* Shows confirmation dialog before clearing location data
*/
confirmEraseLatLong() {
- this.notifyHelpers.confirm(
- "Are you sure you don't want to mark a location? This will erase the current location.",
+ this.notify.confirm(
+ NOTIFY_PROJECT_DELETE_LOCATION_CONFIRM.message,
async () => {
this.eraseLatLong();
},
@@ -827,9 +845,9 @@ export default class NewEditProjectView extends Vue {
* Displays privacy information about partner service integration
*/
public showNostrPartnerInfo() {
- this.notifyHelpers.info(
- "This will submit this project to a partner on the nostr network. It will contain your public key data which may allow correlation, so don't allow this if you're not comfortable with that.",
- TIMEOUTS.VERY_LONG,
+ this.notify.info(
+ NOTIFY_PROJECT_NOSTR_PARTNER_INFO.message,
+ PROJECT_TIMEOUT_VERY_LONG,
);
}