From 7af00aff6f7a068363e956953da25c30309b37b8 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 10 Oct 2026 07:41:07 -0600 Subject: [PATCH] allow changing of the image API server URL --- src/components/ImageMethodDialog.vue | 12 ++--- src/components/PhotoDialog.vue | 12 ++--- src/constants/notifications.ts | 16 ++++++ src/db-sql/migration.ts | 7 +++ src/db/tables/settings.ts | 2 +- src/interfaces/accountView.ts | 3 ++ src/views/AccountViewView.vue | 80 +++++++++++++++++++++++----- src/views/GiftedDetailsView.vue | 13 +++-- src/views/NewEditProjectView.vue | 19 ++++--- src/views/SharedPhotoView.vue | 12 ++--- 10 files changed, 131 insertions(+), 45 deletions(-) diff --git a/src/components/ImageMethodDialog.vue b/src/components/ImageMethodDialog.vue index 74c822a8..a4ab9321 100644 --- a/src/components/ImageMethodDialog.vue +++ b/src/components/ImageMethodDialog.vue @@ -854,19 +854,19 @@ export default class ImageMethodDialog extends Vue { formData.append("image", this.blob, this.fileName || "photo.jpg"); formData.append("claimType", this.claimType); try { + const imageServer = + (await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER; if ( window.location.hostname === "localhost" && - !DEFAULT_IMAGE_API_SERVER.includes("localhost") + !imageServer.includes("localhost") ) { logger.log( "Using shared image API server, so only users on that server can play with images.", ); } - const response = await axios.post( - DEFAULT_IMAGE_API_SERVER + "/image", - formData, - { headers }, - ); + const response = await axios.post(imageServer + "/image", formData, { + headers, + }); this.uploading = false; this.close(); diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue index e0b8ab6f..28862359 100644 --- a/src/components/PhotoDialog.vue +++ b/src/components/PhotoDialog.vue @@ -556,19 +556,19 @@ export default class PhotoDialog extends Vue { formData.append("image", this.blob, this.fileName || "photo.jpg"); formData.append("claimType", this.claimType); try { + const imageServer = + (await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER; if ( window.location.hostname === "localhost" && - !DEFAULT_IMAGE_API_SERVER.includes("localhost") + !imageServer.includes("localhost") ) { logger.log( "Using shared image API server, so only users on that server can play with images.", ); } - const response = await axios.post( - DEFAULT_IMAGE_API_SERVER + "/image", - formData, - { headers }, - ); + const response = await axios.post(imageServer + "/image", formData, { + headers, + }); this.uploading = false; this.close(); diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts index ce252f1d..99cbc76a 100644 --- a/src/constants/notifications.ts +++ b/src/constants/notifications.ts @@ -1401,6 +1401,22 @@ export const NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR = { message: "There was an error deleting the image.", }; +// Used in: AccountViewView.vue, GiftedDetailsView.vue, NewEditProjectView.vue +// (deleteImage - the image server returned 404). The server looks an image up +// by URL and uploader, so this also happens when the image was uploaded to a +// different image server than the one in settings. +export const NOTIFY_IMAGE_DELETE_NOT_FOUND = { + title: "Image Not Deleted", + message: + "The image server in your settings has no record of that image, so it was not removed from storage. It may already be gone, or it may have been uploaded through a different image server.", +}; + +// Used in: GiftedDetailsView.vue (deleteImage method - image deletion error) +export const NOTIFY_IMAGE_DELETE_ERROR = { + title: "Image Deletion Error", + message: "There was an error deleting the image from the image server.", +}; + // Used in: NewEditProjectView.vue (validateLocation method - invalid location error) export const NOTIFY_PROJECT_INVALID_LOCATION = { title: "Invalid Location", diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index 6dcd0751..35c0969f 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -287,6 +287,13 @@ const MIGRATIONS = [ ALTER TABLE settings ADD COLUMN smsNotificationsEnabled BOOLEAN DEFAULT FALSE; `, }, + { + name: "011_add_imageServer_to_settings", + sql: ` + -- Image API server URL; blank means DEFAULT_IMAGE_API_SERVER + ALTER TABLE settings ADD COLUMN imageServer TEXT; + `, + }, ]; /** diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index b74e6cfd..3ec32267 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -38,7 +38,7 @@ export type Settings = { hasBackedUpSeed?: boolean; // tracks whether the user has backed up their seed phrase hideRegisterPromptOnNewContact?: boolean; isRegistered?: boolean; - // imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable + imageServer?: string; // image API server URL lastName?: string; // deprecated - put all names in firstName lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing diff --git a/src/interfaces/accountView.ts b/src/interfaces/accountView.ts index bfd37ece..7c7f4bb1 100644 --- a/src/interfaces/accountView.ts +++ b/src/interfaces/accountView.ts @@ -37,6 +37,7 @@ export interface AccountSettings { notifyingReminderMessage?: string; notifyingReminderTime?: string; starredPlanHandleIds?: string[]; + imageServer?: string; reminderFastRolloverForTesting?: boolean; partnerApiServer?: string; profileImageUrl?: string; @@ -102,6 +103,8 @@ export interface SettingsState { activeDid: string; apiServer: string; apiServerInput: string; + imageServer: string; + imageServerInput: string; partnerApiServer: string; partnerApiServerInput: string; webPushServer: string; diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index a84043ae..29dd4593 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -638,6 +638,7 @@ color="white" aria-hidden="true" > + Unsavedaroonosity
+ + +
+ + When that setting is blank, this app will use the default image server + URL: + {{ DEFAULT_IMAGE_API_SERVER }} +
{ + await this.$saveSettings({ + imageServer: this.imageServerInput, + }); + this.imageServer = this.imageServerInput; + + await this.$saveUserSettings(this.activeDid, { + imageServer: this.imageServer, + }); + } + async onClickSavePushServer(): Promise { await this.$saveSettings({ webPushServer: this.webPushServerInput, @@ -2583,15 +2640,14 @@ export default class AccountViewView extends Vue { async deleteImage(): Promise { try { - // Extract the image ID from the full URL - const imageId = this.profileImageUrl?.split("/").pop(); - if (!imageId) { + if (!this.profileImageUrl) { this.notify.error("Invalid image URL"); return; } + // the image server looks the image up by its full URL const response = await this.axios.delete( - this.apiServer + "/api/image/" + imageId, + this.imageServer + "/image/" + encodeURIComponent(this.profileImageUrl), { headers: await getHeaders(this.activeDid) }, ); if (response.status === 204) { @@ -2607,12 +2663,12 @@ export default class AccountViewView extends Vue { } } catch (error) { if (isApiError(error) && error.response?.status === 404) { - // it already doesn't exist so we won't say anything to the user - // Clear the local reference since the image is gone + // still drop it from the profile, which is what the user asked for this.profileImageUrl = ""; await this.$saveSettings({ profileImageUrl: "", }); + this.notify.error(NOTIFY_IMAGE_DELETE_NOT_FOUND.message, TIMEOUTS.LONG); } else { this.notify.error( ACCOUNT_VIEW_CONSTANTS.ERRORS.IMAGE_DELETE_ERROR, diff --git a/src/views/GiftedDetailsView.vue b/src/views/GiftedDetailsView.vue index e681083c..75f849bf 100644 --- a/src/views/GiftedDetailsView.vue +++ b/src/views/GiftedDetailsView.vue @@ -271,6 +271,8 @@ import { NOTIFY_GIFTED_DETAILS_CREATE_GIVE_ERROR, NOTIFY_GIFTED_DETAILS_GIFT_RECORDED, NOTIFY_GIFT_ERROR_NO_DESCRIPTION, + NOTIFY_IMAGE_DELETE_ERROR, + NOTIFY_IMAGE_DELETE_NOT_FOUND, } from "@/constants/notifications"; @Component({ @@ -563,30 +565,33 @@ export default class GiftedDetails extends Vue { } try { const headers = await getHeaders(this.activeDid); + const imageServer = + (await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER; if ( window.location.hostname === "localhost" && - !DEFAULT_IMAGE_API_SERVER.includes("localhost") + !imageServer.includes("localhost") ) { logger.log( "Using shared image API server, so only users on that server can play with images.", ); } const response = await this.axios.delete( - DEFAULT_IMAGE_API_SERVER + "/image/" + encodeURIComponent(imageUrl), + imageServer + "/image/" + encodeURIComponent(imageUrl), { headers }, ); if (response.status === 204) { // don't bother with a notification } else { logger.error("Problem deleting image:", response); + this.notify.error(NOTIFY_IMAGE_DELETE_ERROR.message, TIMEOUTS.LONG); } } catch (error) { logger.error("Error deleting image:", error); // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((error as any)?.response?.status === 404) { - logger.log("Image was already deleted:", error); + this.notify.error(NOTIFY_IMAGE_DELETE_NOT_FOUND.message, TIMEOUTS.LONG); } else { - logger.error("Failed to delete image from server:", error); + this.notify.error(NOTIFY_IMAGE_DELETE_ERROR.message, TIMEOUTS.LONG); } } } diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index dae7d3fd..f5556f6b 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -335,13 +335,14 @@ import { NotificationIface, } from "../constants/app"; import { PlatformServiceMixin } from "../utils/PlatformServiceMixin"; -import { createNotifyHelpers } from "../utils/notify"; +import { createNotifyHelpers, TIMEOUTS } 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_IMAGE_DELETE_NOT_FOUND, NOTIFY_PROJECT_INVALID_LOCATION, NOTIFY_PROJECT_INVALID_START_DATE, NOTIFY_PROJECT_INVALID_END_DATE, @@ -617,9 +618,11 @@ export default class NewEditProjectView extends Vue { } try { const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders; + const imageServer = + (await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER; if ( window.location.hostname === "localhost" && - !DEFAULT_IMAGE_API_SERVER.includes("localhost") + !imageServer.includes("localhost") ) { logger.log( "Using shared image API server, so only users on that server can play with images.", @@ -627,9 +630,7 @@ export default class NewEditProjectView extends Vue { } const response = await this.axios.delete( - DEFAULT_IMAGE_API_SERVER + - "/image/" + - encodeURIComponent(this.imageUrl), + imageServer + "/image/" + encodeURIComponent(this.imageUrl), { headers }, ); if (response.status === 204) { @@ -645,12 +646,10 @@ export default class NewEditProjectView extends Vue { } catch (error) { logger.error("Error deleting image:", error); // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((error as any).response.status === 404) { - logger.log("The image was already deleted:", error); - + if ((error as any)?.response?.status === 404) { + // still drop it from the project, which is what the user asked for this.imageUrl = ""; - - // it already doesn't exist so we won't say anything to the user + this.notify.error(NOTIFY_IMAGE_DELETE_NOT_FOUND.message, TIMEOUTS.LONG); } else { this.notify.error(NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR.message); } diff --git a/src/views/SharedPhotoView.vue b/src/views/SharedPhotoView.vue index 78d7d7d7..d35e39b2 100644 --- a/src/views/SharedPhotoView.vue +++ b/src/views/SharedPhotoView.vue @@ -338,19 +338,19 @@ export default class SharedPhotoView extends Vue { ); formData.append("claimType", imageType); + const imageServer = + (await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER; if ( window.location.hostname === "localhost" && - !DEFAULT_IMAGE_API_SERVER.includes("localhost") + !imageServer.includes("localhost") ) { logger.log( "Using shared image API server, so only users on that server can play with images.", ); } - const response = await axios.post( - DEFAULT_IMAGE_API_SERVER + "/image", - formData, - { headers }, - ); + const response = await axios.post(imageServer + "/image", formData, { + headers, + }); if (response?.data?.url) { this.imageBlob = undefined; this.imageFileName = undefined;