diff --git a/src/main/core/api/files.ts b/src/main/core/api/files.ts index c9e8186..f3c0340 100644 --- a/src/main/core/api/files.ts +++ b/src/main/core/api/files.ts @@ -1,12 +1,33 @@ import { API_BASE_URL } from "@/core/config"; import { getAuthHeaders } from "./user/auth"; +/** + * Resolve a server attachment path to a fetchable URL. + * Strips a legacy `/api` prefix, then joins to the API base (or same-origin `/api` in the browser). + */ +export function resolveAttachmentUrl(path: string): string { + if (/^https?:\/\//i.test(path)) return path; + let relative = path.startsWith("/") ? path : `/${path}`; + if (relative.startsWith("/api/") || relative === "/api") { + relative = relative.slice(4) || "/"; + } + // Same-origin `/api` keeps Vite/Caddy proxies working for + cookie auth. + if (typeof window !== "undefined" && !API_BASE_URL.startsWith("http")) { + const base = API_BASE_URL.replace(/\/$/, "") || "/api"; + return `${base}${relative}`; + } + if (typeof window !== "undefined") { + return `/api${relative}`; + } + return `${API_BASE_URL.replace(/\/$/, "")}${relative}`; +} + export const normal = { /** * Gets the URL for a normal (unencrypted) file */ url(filename: string): string { - return `${API_BASE_URL}/uploads/files/normal/${filename}`; + return resolveAttachmentUrl(`/uploads/files/normal/${filename}`); }, /** @@ -26,7 +47,7 @@ export const encrypted = { * Gets the URL for an encrypted file */ url(filename: string): string { - return `${API_BASE_URL}/uploads/files/encrypted/${filename}`; + return resolveAttachmentUrl(`/uploads/files/encrypted/${filename}`); }, /** diff --git a/src/main/pages/chat/ui/right/Message.tsx b/src/main/pages/chat/ui/right/Message.tsx index ba916ec..7ae4c30 100644 --- a/src/main/pages/chat/ui/right/Message.tsx +++ b/src/main/pages/chat/ui/right/Message.tsx @@ -230,7 +230,7 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD try { // no-op decrypt indicator removed from UI // Fetch encrypted file - const response = await fetch(file.path, { + const response = await fetch(api.files.resolveAttachmentUrl(file.path), { headers: api.user.auth.getAuthHeaders(user.authToken!) }); if (!response.ok) throw new Error("Failed to fetch file"); @@ -425,7 +425,7 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD } // If not decrypted or public file, fetch with credentials/headers - const response = await fetch(file.path, { + const response = await fetch(api.files.resolveAttachmentUrl(file.path), { headers: user.authToken ? api.user.auth.getAuthHeaders(user.authToken) : undefined, credentials: "include" }); @@ -585,7 +585,11 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD const looksEncryptedPath = /\/uploads\/files\/encrypted\//.test(file.path) || /\/api\/uploads\/files\/encrypted\//.test(file.path); const isEncryptedDm = Boolean(isDm && (file.encrypted || looksEncryptedPath)); const decryptedUrl = decryptedFiles.get(file.path); - const imageSrc = isImage ? (isEncryptedDm ? decryptedUrl : file.path) : undefined; + const imageSrc = isImage + ? (isEncryptedDm + ? decryptedUrl + : api.files.resolveAttachmentUrl(file.path)) + : undefined; const isDownloading = downloadingPaths.has(file.path); const isSending = message.runtimeData?.sendingState?.status === 'sending';