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}
-
-
{'\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}
+

+
+ {: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();