From ea7895d85c160307d97703039e65b11bd4da7299 Mon Sep 17 00:00:00 2001 From: JustMe Date: Sat, 17 Oct 2026 21:37:30 +0800 Subject: [PATCH] feat(web): upload large avatars in two steps so the thumbnail applies immediately --- web/src/core-bridge/worker/account.ts | 29 +++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/web/src/core-bridge/worker/account.ts b/web/src/core-bridge/worker/account.ts index b820ba5..42cef8a 100644 --- a/web/src/core-bridge/worker/account.ts +++ b/web/src/core-bridge/worker/account.ts @@ -233,13 +233,21 @@ export async function clearSavedLogin(): Promise { } await forgetAccount(); await forgetLoginFields(); } +/** 原图超过这个大小(字符数)就分两步传:先传缩略图,头像立刻生效,原图在后台慢慢传(弱网下 MB 级的动图一次传完要等很久) */ +const SPLIT_UPLOAD_CHARS = 600_000; + export async function updatePersonal(displayName: string, change?: AvatarChange): Promise { if (!account) throw new Error('请先登录账号'); const session = account; + const send = async (request: AccountProfileRequest) => { + const { body, headers: encoding } = await jsonBody(request); + return apiFetch('/accounts/profile', { method: 'PATCH', headers: { ...headers(session), ...encoding }, body }).then(r => r.json() as Promise); + }; + const split = change && change !== 'keep' && change.avatar.length > SPLIT_UPLOAD_CHARS; const request: AccountProfileRequest = change === 'keep' ? { displayName, keepAvatar: true } + : split ? { displayName, keepAvatar: false, avatar: change.thumb, avatarThumb: change.thumb } : change ? { displayName, keepAvatar: false, avatar: change.avatar, avatarThumb: change.thumb } : { displayName, keepAvatar: false }; - const { body, headers: encoding } = await jsonBody(request); - const value = await apiFetch('/accounts/profile', { method: 'PATCH', headers: { ...headers(session), ...encoding }, body }).then(r => r.json() as Promise); + const value = await send(request); stillActive(session); const previous = session.personal?.avatar; session.personal = personalProfile(value); @@ -249,6 +257,23 @@ export async function updatePersonal(displayName: string, change?: AvatarChange) primeAvatar(value.username, value.modifiedAt, shown); setAccountIdentity(session, session.profile.keys.publicKey, session.personal); await persistSession(session, { vault: await sealJson(session.profile, session.key), revision: session.revision }); + if (split) void uploadOriginal(session, displayName, change, send); +} + +/** 后台补传原图;失败就保留缩略图当头像(预览时看到的是缩略图),不打扰用户。重试几次,间隔逐步拉长 */ +async function uploadOriginal(session: NonNullable, displayName: string, change: { avatar: string; thumb: string }, send: (request: AccountProfileRequest) => Promise): Promise { + for (const wait of [0, 5_000, 20_000, 60_000]) { + if (wait) await new Promise((resolve) => setTimeout(resolve, wait)); + if (account !== session) return; + try { + const value = await send({ displayName, keepAvatar: false, avatar: change.avatar, avatarThumb: change.thumb }); + if (account !== session) return; + session.personal = { ...personalProfile(value), avatar: change.thumb, hasAvatar: true }; + primeAvatar(value.username, value.modifiedAt, change.thumb); + setAccountIdentity(session, session.profile.keys.publicKey, session.personal); + return; + } catch { /* 弱网或暂时失败:稍后重试 */ } + } } export async function listAccountDevices(): Promise { requireAccount(); return apiFetch('/accounts/devices').then((r) => r.json() as Promise); } export async function renameAccountDevice(id: string, name: string): Promise {