diff --git a/src/api/client.js b/src/api/client.js index 13b677f..576b1ba 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -17,6 +17,71 @@ function authHeaders() { } } + +function isAssetRef(value) { + return typeof value === "string" && value.startsWith("asset://"); +} + +async function resolveAssetURLs(ids) { + if (!ids || !ids.length) return {}; + const qs = new URLSearchParams(); + qs.set("ids", ids.join(",")); + try { + const res = await fetch(`${API_BASE}/assets/resolve?${qs.toString()}`, { + credentials: "include", + headers: { ...authHeaders() }, + }); + if (!res.ok) return {}; + const data = await res.json(); + return (data && data.urls) || {}; + } catch (err) { + console.warn("resolveAssetURLs error:", err); + return {}; + } +} + +async function resolveAssetRefsInPosts(posts) { + if (!Array.isArray(posts) || posts.length === 0) return posts; + const ids = []; + for (const p of posts) { + [p?.image_small, p?.image_large, p?.image, p?.media_url].forEach((v) => { + if (isAssetRef(v)) ids.push(v.replace("asset://", "")); + }); + } + const uniq = Array.from(new Set(ids)); + if (!uniq.length) return posts; + const resolved = await resolveAssetURLs(uniq); + return posts.map((p) => { + const clone = { ...p }; + ["image_small", "image_large", "image", "media_url"].forEach((k) => { + const v = clone[k]; + if (isAssetRef(v)) { + const id = v.replace("asset://", ""); + if (resolved[id]) clone[k] = resolved[id]; + } + }); + return clone; + }); +} + +async function resolveAssetRefsInIslands(islands) { + if (!Array.isArray(islands) || islands.length === 0) return islands; + const ids = []; + for (const i of islands) { + if (isAssetRef(i?.avatar_url)) ids.push(i.avatar_url.replace("asset://", "")); + } + const uniq = Array.from(new Set(ids)); + if (!uniq.length) return islands; + const resolved = await resolveAssetURLs(uniq); + return islands.map((i) => { + if (isAssetRef(i?.avatar_url)) { + const id = i.avatar_url.replace("asset://", ""); + if (resolved[id]) return { ...i, avatar_url: resolved[id] }; + } + return i; + }); +} + /** * Récupère la liste des posts, avec filtres optionnels. */ @@ -45,7 +110,8 @@ export async function fetchPosts(filters = {}) { } const data = await res.json(); - return Array.isArray(data) ? data : data.items || []; + const items = Array.isArray(data) ? data : data.items || []; + return await resolveAssetRefsInPosts(items); } /** @@ -155,7 +221,9 @@ export async function fetchPostById(postId) { }); if (!res.ok) return null; const data = await res.json().catch(() => null); - return data && typeof data === "object" ? data : null; + if (!data || typeof data !== "object") return null; + const resolved = await resolveAssetRefsInPosts([data]); + return resolved && resolved[0] ? resolved[0] : data; } catch { return null; } @@ -219,7 +287,8 @@ export async function fetchSmartFeed(params = {}) { return []; } const data = await res.json(); - return Array.isArray(data) ? data : data.results || data.items || []; + const items = Array.isArray(data) ? data : data.results || data.items || []; + return await resolveAssetRefsInPosts(items); } catch (err) { console.warn("fetchSmartFeed error:", err); return []; @@ -281,7 +350,12 @@ export async function pollNewPosts(params = {}) { console.warn("pollNewPosts failed", res.status); return { posts: [], count: 0, max_id: 0 }; } - return await res.json(); + const data = await res.json(); + if (data && data.avatar_url && isAssetRef(data.avatar_url)) { + const resolved = await resolveAssetRefsInIslands([data]); + return resolved[0] || data; + } + return data; } catch (err) { console.warn("pollNewPosts error:", err); return { posts: [], count: 0, max_id: 0 }; @@ -312,7 +386,8 @@ export async function fetchIslands(params = {}) { return []; } const data = await res.json(); - return Array.isArray(data) ? data : data.islands || []; + const items = Array.isArray(data) ? data : data.islands || []; + return await resolveAssetRefsInIslands(items); } catch (err) { console.warn("fetchIslands error:", err); return []; @@ -335,7 +410,12 @@ export async function fetchIsland(islandId) { console.warn("fetchIsland failed", res.status); return null; } - return await res.json(); + const data = await res.json(); + if (data && data.avatar_url && isAssetRef(data.avatar_url)) { + const resolved = await resolveAssetRefsInIslands([data]); + return resolved[0] || data; + } + return data; } catch (err) { console.warn("fetchIsland error:", err); return null; @@ -351,18 +431,26 @@ export async function fetchIslandByUsername(username) { console.warn("fetchIslandByUsername failed", res.status); return null; } - return await res.json(); + const data = await res.json(); + if (data && data.avatar_url && isAssetRef(data.avatar_url)) { + const resolved = await resolveAssetRefsInIslands([data]); + return resolved[0] || data; + } + return data; } catch (err) { console.warn("fetchIslandByUsername error:", err); return null; } } -export async function uploadProfileAvatar(file) { +export async function uploadProfileAvatar(file, opts = {}) { if (!file) return { ok: false }; const url = `${API_BASE}/profile/avatar`; const form = new FormData(); form.append("file", file); + if (opts.visibility) form.append("visibility", opts.visibility); + if (opts.group_id) form.append("group_id", opts.group_id); + if (opts.users && opts.users.length) form.append("users", opts.users.join(",")); try { const res = await fetch(url, { method: "POST", diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx index ff71f70..dede66b 100644 --- a/src/components/Map/MapView.jsx +++ b/src/components/Map/MapView.jsx @@ -121,6 +121,9 @@ export default function MapView({ const [draftImageFile, setDraftImageFile] = useState(null); const [draftImagePreview, setDraftImagePreview] = useState(""); const [useCustomImage, setUseCustomImage] = useState(false); + const [imageVisibility, setImageVisibility] = useState("public"); + const [imageGroupId, setImageGroupId] = useState(""); + const [imageUsers, setImageUsers] = useState(""); const [uploadingImage, setUploadingImage] = useState(false); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(""); @@ -493,6 +496,9 @@ export default function MapView({ setDraftImageFile(null); setDraftImagePreview(""); setUseCustomImage(false); + setImageVisibility("public"); + setImageGroupId(""); + setImageUsers(""); const main = mainFilter === "All" ? "News" : mainFilter; setDraftCategory(main); const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News; @@ -539,9 +545,12 @@ export default function MapView({ try { const formData = new FormData(); - formData.append('image', file); + formData.append('file', file); + formData.append('visibility', imageVisibility); + if (imageGroupId.trim()) formData.append('group_id', imageGroupId.trim()); + if (imageUsers.trim()) formData.append('users', imageUsers.trim()); - const res = await fetch('/api/upload-image', { + const res = await fetch('/api/assets/upload', { method: 'POST', body: formData, credentials: 'include', @@ -553,7 +562,11 @@ export default function MapView({ } const data = await res.json(); - setDraftImage(data.url || data.image_url); + if (data && data.url) { + setDraftImage(data.url); + } else if (data && data.asset_id) { + setDraftImage(`asset://${data.asset_id}`); + } setUploadingImage(false); } catch (err) { console.error('Image upload error:', err); @@ -815,7 +828,7 @@ export default function MapView({ {useCustomImage && ( -