feat(web): avatar thumbnails with original preview, registration avatar, pure-JS crypto fallback for http [CI]
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was successful

This commit is contained in:
justme committed 2026-10-17 14:35:42 +08:00
1 parent 283196938e
commit d66c93fe98
36 files changed
+735 -60

No files matched your search

+15 -5
View File
@@ -1,5 +1,5 @@
import { importResources, importResourceDeletes, listAllResources, listResourceDeletes, setResourceChangeListener, setResourceScope, type SavedResource } from '../database';
import { apiFetch } from './http';
import { apiFetch, jsonBody } from './http';
import { clearResourceSessions, getSession } from './sessions';
import { setAccountIdentity, requireAccount } from './accountSession';
import { b64, unb64, deriveAccountKeys, sealJson, openJson, generateInvitationKeys, generateAccountSigningKeys } from './accountCrypto';
@@ -11,6 +11,8 @@ import { forgetAccount, localAccountDevice, rememberedAccount, rememberAccount,
import type { AccountVaultBody, AccountSaltBody, AccountSessionBody, PublicKeyBody, InvitationBody, InvitationCryptoPlan, AccountDeviceBody, AccountRegisterRequest, AccountSessionRequest } from '../generated/api';
import { loadPersonal, personalProfile } from './accountProfile';
import { primeAvatar } from './avatars';
import type { AvatarChange } from '../protocol/account';
import type { AccountProfile, AccountProfileRequest } from '../generated/api';
import type { PersonalProfile } from '../protocol/account';
@@ -206,7 +208,8 @@ export async function unlockAuthenticatedAccount(identity: AccountSessionBody, k
export async function accountStatus(): Promise<{ username: string | null; revision: number; remembered: boolean; personal?: PersonalProfile; error?: string; unlockUsername?: string }> {
return { username: account?.username ?? null, revision: account?.revision ?? 0,
remembered: account?.remember ?? false, unlockUsername: passkeyUnlockUsername(),
...(account?.personal ? { personal: account.personal } : {}),
// 界面每隔几秒就读一次状态:不能每次都带着几 MB 的头像过 postMessage,头像由界面另外取
...(account?.personal ? { personal: { displayName: account.personal.displayName, modifiedAt: account.personal.modifiedAt, hasAvatar: account.personal.hasAvatar ?? Boolean(account.personal.avatar) } } : {}),
...(account?.error ? { error: '账号资源同步失败,请重试。' } : {}) };
}
export async function logoutAccount(): Promise<void> {
@@ -230,13 +233,20 @@ export async function clearSavedLogin(): Promise<void> {
}
await forgetAccount(); await forgetLoginFields();
}
export async function updatePersonal(displayName: string, avatar?: string): Promise<void> {
export async function updatePersonal(displayName: string, change?: AvatarChange): Promise<void> {
if (!account) throw new Error('请先登录账号');
const session = account;
const value = await apiFetch('/accounts/profile', { method: 'PATCH', headers: { ...headers(session), 'Content-Type': 'application/json' },
body: JSON.stringify({ displayName, avatar } satisfies AccountProfileRequest) }).then(r => r.json() as Promise<AccountProfile>);
const request: AccountProfileRequest = change === 'keep' ? { displayName, keepAvatar: true }
: 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<AccountProfile>);
stillActive(session);
const previous = session.personal?.avatar;
session.personal = personalProfile(value);
// 本机刚设置的头像直接用,不用再下载;只改名字时沿用现有的
const shown = change && change !== 'keep' ? change.thumb : change === 'keep' ? previous : undefined;
if (shown) { session.personal.avatar = shown; session.personal.hasAvatar = true; }
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 });
}