From 670f9d73f304801f3befaf15eac78ce8825d33d0 Mon Sep 17 00:00:00 2001 From: StroepWafel Date: Thu, 25 Jun 2026 18:36:57 +0930 Subject: [PATCH] fix(cloud PFP): Not pulling from cloud on page load --- .../components/CloudPfpAvatar.svelte | 8 +- .../ProfilePictureSetting.svelte | 125 ++++++++++----- src/plugins/built-in/profilePicture/index.ts | 26 +-- src/seqta/utils/cloudPfpCache.ts | 8 + src/seqta/utils/cloudPfpSync.ts | 149 +++++++++++++++--- 5 files changed, 249 insertions(+), 67 deletions(-) diff --git a/src/interface/components/CloudPfpAvatar.svelte b/src/interface/components/CloudPfpAvatar.svelte index d44a181a..c8bf4891 100644 --- a/src/interface/components/CloudPfpAvatar.svelte +++ b/src/interface/components/CloudPfpAvatar.svelte @@ -1,5 +1,5 @@ -
value ? null : triggerSelect()} - ondragover={(e) => { e.stopPropagation(); dragging = true }} - ondragleave={() => dragging = false} - ondrop={onDrop} - onkeydown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - triggerSelect() - } - }} - role="button" - tabindex="0" -> - {#if value} - Profile - - {:else} -
- {'\ued47'} - Upload +
+ {#if useCloudPfp} +
+ +
+ {#if cloudRefreshError} +

{cloudRefreshError}

+ {/if} {/if} - - {#if dragging} -
- {/if} + +
value ? null : triggerSelect()} + ondragover={(e) => { e.stopPropagation(); dragging = true }} + ondragleave={() => dragging = false} + ondrop={onDrop} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + triggerSelect() + } + }} + role="button" + tabindex="0" + > + {#if value} + Profile + + {:else} +
+ {'\ued47'} + Upload +
+ {/if} + + {#if dragging} +
+ {/if} +
diff --git a/src/plugins/built-in/profilePicture/index.ts b/src/plugins/built-in/profilePicture/index.ts index 450b567e..4d5ad7b7 100644 --- a/src/plugins/built-in/profilePicture/index.ts +++ b/src/plugins/built-in/profilePicture/index.ts @@ -8,7 +8,7 @@ import ProfilePictureSetting from "./ProfilePictureSetting.svelte"; import { waitForElm } from "@/seqta/utils/waitForElm"; import browser from "webextension-polyfill"; import { cloudAuth } from "@/seqta/utils/CloudAuth"; -import { resolveCloudPfp } from "@/seqta/utils/cloudPfpCache"; +import { resolveCloudPfp, defaultAccountsPfpUrl } from "@/seqta/utils/cloudPfpCache"; import styles from "./styles.css?inline"; import localforage from "localforage"; @@ -64,10 +64,13 @@ const profilePicturePlugin: Plugin = { } const useCloud = api.settings.useCloudPfp; - const pfpUrl = cloudAuth.state.user?.pfpUrl; + const userId = cloudAuth.state.user?.id; + const pfpUrl = + cloudAuth.state.user?.pfpUrl ?? + (userId ? defaultAccountsPfpUrl(userId) : undefined); - if (useCloud && pfpUrl && cloudAuth.state.user?.id) { - const resolved = await resolveCloudPfp(cloudAuth.state.user.id, pfpUrl); + if (useCloud && pfpUrl && userId) { + const resolved = await resolveCloudPfp(userId, pfpUrl); if (resolved) { currentBlobUrl = resolved.src; img = document.createElement("img"); @@ -92,6 +95,13 @@ const profilePicturePlugin: Plugin = { } } + if (api.settings.useCloudPfp && cloudAuth.state.isLoggedIn) { + const { pullCloudProfilePictureFromServer } = await import( + "@/seqta/utils/cloudPfpSync" + ); + await pullCloudProfilePictureFromServer(); + } + await applyProfileImage(); const onLocalPictureUpdated = () => { @@ -114,11 +124,9 @@ const profilePicturePlugin: Plugin = { }); const useCloudUnreg = api.settings.onChange("useCloudPfp", (enabled: boolean) => { - if (enabled) { - void import("@/seqta/utils/cloudPfpSync").then(({ syncLocalProfilePictureToCloud }) => - syncLocalProfilePictureToCloud(), - ); - } + void import("@/seqta/utils/cloudPfpSync").then(({ onUseCloudPfpToggled }) => + onUseCloudPfpToggled(enabled), + ); void applyProfileImage(); }); diff --git a/src/seqta/utils/cloudPfpCache.ts b/src/seqta/utils/cloudPfpCache.ts index bbb3b560..64c86cfc 100644 --- a/src/seqta/utils/cloudPfpCache.ts +++ b/src/seqta/utils/cloudPfpCache.ts @@ -28,6 +28,10 @@ export function pfpUrlWithHash(url: string, hash: string | null | undefined): st return `${base}?v=${hash}`; } +export function defaultAccountsPfpUrl(userId: string): string { + return `${ACCOUNTS_BASE}/api/user/pfp/${userId}`; +} + async function fetchServerHash(userId: string): Promise { const res = await fetch(`${ACCOUNTS_BASE}/api/user/pfp/${userId}/meta`); if (!res.ok) return null; @@ -95,6 +99,10 @@ export async function resolveCloudPfp( if (localHash) { headers["If-None-Match"] = `"${localHash}"`; } + const token = await cloudAuth.getStoredToken(); + if (token && isAccountsHostedPfpUrl(pfpUrl)) { + headers.Authorization = `Bearer ${token}`; + } const res = await fetch(imageUrl, { headers }); if (res.status === 304 && localBlob instanceof Blob) { diff --git a/src/seqta/utils/cloudPfpSync.ts b/src/seqta/utils/cloudPfpSync.ts index 2271307c..9532597e 100644 --- a/src/seqta/utils/cloudPfpSync.ts +++ b/src/seqta/utils/cloudPfpSync.ts @@ -1,7 +1,10 @@ import browser from "webextension-polyfill"; import localforage from "localforage"; -import { cloudAuth } from "@/seqta/utils/CloudAuth"; -import { clearCloudPfpCache, pfpUrlWithHash } from "@/seqta/utils/cloudPfpCache"; +import { cloudAuth, type CloudUser } from "@/seqta/utils/CloudAuth"; +import { + clearCloudPfpCache, + pfpUrlWithHash, +} from "@/seqta/utils/cloudPfpCache"; const ACCOUNTS_BASE = "https://accounts.betterseqta.org"; const PLUGIN_SETTINGS_KEY = "plugin.profile-picture.settings"; @@ -54,6 +57,92 @@ async function parseJsonResponse(r: Response): Promise> } } +function mergeMeIntoUser(current: CloudUser, data: Record): CloudUser { + const raw = (data.user as Record | undefined) ?? data; + const pfpUrlRaw = raw.pfpUrl as string | null | undefined; + const pfpHash = (raw.pfpHash as string | null | undefined) ?? null; + const pfpUrl = + pfpUrlRaw == null || pfpUrlRaw === "" + ? undefined + : pfpUrlWithHash(pfpUrlRaw, pfpHash); + + return { + ...current, + email: (raw.email as string | undefined) ?? current.email, + username: (raw.username as string | undefined) ?? current.username, + displayName: (raw.displayName as string | undefined) ?? current.displayName, + admin_level: (raw.admin_level as number | undefined) ?? current.admin_level, + pfpUrl, + pfpHash, + }; +} + +/** Fetch `/api/auth/me` and update stored user (pfpUrl / pfpHash). */ +export async function refreshCloudUserFromServer(): Promise<{ + success: boolean; + error?: string; +}> { + if (!cloudAuth.state.isLoggedIn) { + return { success: false, error: "Not signed in to BetterSEQTA Cloud" }; + } + + const token = await cloudAuth.getStoredToken(); + if (!token) return { success: false, error: "Not signed in to BetterSEQTA Cloud" }; + + const current = cloudAuth.state.user; + if (!current?.id) return { success: false, error: "No cloud user on this device" }; + + try { + const res = await fetch(`${ACCOUNTS_BASE}/api/auth/me`, { + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await parseJsonResponse(res); + if (!res.ok) { + return { + success: false, + error: (data.error as string) ?? `Could not refresh account (${res.status})`, + }; + } + + await cloudAuth.setUser(mergeMeIntoUser(current, data)); + return { success: true }; + } catch (err) { + return { + success: false, + error: err instanceof Error ? err.message : "Could not refresh account", + }; + } +} + +/** Pull cloud avatar metadata from the server and refresh the in-page profile image. */ +export async function pullCloudProfilePictureFromServer(): Promise<{ + success: boolean; + error?: string; +}> { + const refreshed = await refreshCloudUserFromServer(); + if (!refreshed.success) return refreshed; + + const userId = cloudAuth.state.user?.id; + if (userId) await clearCloudPfpCache(userId); + await notifyProfilePictureChanged(); + return { success: true }; +} + +/** When cloud PFP is enabled: upload local image if present, otherwise pull from server. */ +export async function onUseCloudPfpToggled(enabled: boolean): Promise { + if (!enabled) { + await notifyProfilePictureChanged(); + return; + } + + const blob = await profileStore.getItem("profile-picture"); + if (blob instanceof Blob) { + await syncLocalProfilePictureToCloud(); + } else { + await pullCloudProfilePictureFromServer(); + } +} + export async function syncLocalProfilePictureToCloud(): Promise<{ success: boolean; error?: string; @@ -72,22 +161,7 @@ export async function syncLocalProfilePictureToCloud(): Promise<{ try { if (!blob || !(blob instanceof Blob)) { - const res = await fetch(`${ACCOUNTS_BASE}/api/user/pfp/clear`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }); - const data = await parseJsonResponse(res); - if (!res.ok) { - return { success: false, error: (data.error as string) ?? `Clear failed (${res.status})` }; - } - if (user) { - await cloudAuth.setUser({ ...user, pfpUrl: undefined, pfpHash: null }); - } - if (userId) await clearCloudPfpCache(userId); + // No local upload — keep the server avatar; do not clear cloud. return { success: true }; } @@ -131,6 +205,45 @@ export async function syncLocalProfilePictureToCloud(): Promise<{ } } +/** Upload local image to cloud, or clear cloud only when explicitly removing local image. */ +export async function clearCloudProfilePicture(): Promise<{ + success: boolean; + error?: string; +}> { + if (!cloudAuth.state.isLoggedIn) return { success: true }; + + const token = await cloudAuth.getStoredToken(); + if (!token) return { success: false, error: "Not logged in" }; + + const user = cloudAuth.state.user; + const userId = user?.id; + + try { + const res = await fetch(`${ACCOUNTS_BASE}/api/user/pfp/clear`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }); + const data = await parseJsonResponse(res); + if (!res.ok) { + return { success: false, error: (data.error as string) ?? `Clear failed (${res.status})` }; + } + if (user) { + await cloudAuth.setUser({ ...user, pfpUrl: undefined, pfpHash: null }); + } + if (userId) await clearCloudPfpCache(userId); + return { success: true }; + } catch (err) { + return { + success: false, + error: err instanceof Error ? err.message : "Cloud profile picture clear failed", + }; + } +} + /** Notify SEQTA content scripts to refresh the in-page profile image. */ export async function notifyProfilePictureChanged(): Promise { const revision = Date.now();