diff --git a/web/src/features/settings/PrivacySettingsPage.tsx b/web/src/features/settings/PrivacySettingsPage.tsx index c869cf75..f26cded8 100644 --- a/web/src/features/settings/PrivacySettingsPage.tsx +++ b/web/src/features/settings/PrivacySettingsPage.tsx @@ -62,7 +62,12 @@ export function PrivacySettingsPage() { // re-enables every field a tenant policy had actually locked, until an unrelated refetch happens // to restore it. onSuccess: (data) => queryClient.setQueryData(['my-privacy'], (prev: typeof query.data) => (prev ? { ...prev, ...data } : data)), - onError: (err) => setError(err instanceof Error ? err.message : '设置失败'), + // Same reasoning as ProfileSettingsPage's own saveMutation: a version conflict shouldn't leave + // the cached `version` stale, or every retry just resends the same rejected value forever. + onError: (err) => { + setError(err instanceof Error ? err.message : '设置失败'); + queryClient.invalidateQueries({ queryKey: ['my-privacy'] }); + }, }); const patch = (partial: Partial[0], 'version'>>) => { if (!query.data) return; @@ -113,7 +118,7 @@ export function PrivacySettingsPage() {
-
+
资料可见范围
{VISIBILITY_OPTIONS.map((opt) => ( @@ -129,7 +134,7 @@ export function PrivacySettingsPage() { ))}
-
+
最近在线可见范围
{VISIBILITY_OPTIONS.map((opt) => ( diff --git a/web/src/features/settings/ProfileSettingsPage.tsx b/web/src/features/settings/ProfileSettingsPage.tsx index 3852fc7b..304cde5c 100644 --- a/web/src/features/settings/ProfileSettingsPage.tsx +++ b/web/src/features/settings/ProfileSettingsPage.tsx @@ -32,9 +32,14 @@ const InlineField = forwardRef (prev ? { ...prev, ...data } : data)); queryClient.invalidateQueries({ queryKey: ['me'] }); }, - onError: (err) => setError(err instanceof Error ? err.message : '保存失败'), + // IM-前后端对接文档.md 10.3 (版本冲突先拉最新资料再由用户确认): a stale `version` (e.g. edited + // on another tab/device) 400s here, but the cached `query.data.version` used to just sit there + // unchanged -- every retry would resend the exact same stale version and 400 again forever, the + // toast's own "请刷新后重试" copy notwithstanding, since nothing actually refetched. + onError: (err) => { + setError(err instanceof Error ? err.message : '保存失败'); + queryClient.invalidateQueries({ queryKey: ['my-profile'] }); + }, }); const avatarMutation = useMutation({ mutationFn: async (file: File) => { - const uploaded = await uploadFile(file); + const uploaded = await uploadFile(file, undefined, 'user_avatar'); if (!query.data) throw new Error('尚未加载'); return updateMyProfile({ avatar_file_id: uploaded.file_id, version: query.data.version }); }, @@ -172,7 +184,7 @@ export function ProfileSettingsPage() { {showQrCode ? setShowQrCode(false)} /> : null}
-
+
昵称
-
+
性别
{GENDER_OPTIONS.map((opt) => ( @@ -199,7 +211,7 @@ export function ProfileSettingsPage() {
-
+
出生日期
-
+
地区 - setDraft({ ...draft, region: e.target.value })} placeholder="未填写" /> + setDraft({ ...draft, region: e.target.value })} placeholder="未填写" maxLength={50} />
diff --git a/web/src/features/settings/SecuritySettingsPage.tsx b/web/src/features/settings/SecuritySettingsPage.tsx index 51cb4608..473525e3 100644 --- a/web/src/features/settings/SecuritySettingsPage.tsx +++ b/web/src/features/settings/SecuritySettingsPage.tsx @@ -95,7 +95,14 @@ export function SecuritySettingsPage() {
{changingPassword ? ( -
+
{ + e.preventDefault(); + if (currentPassword.length === 0 || newPassword.length < minPasswordLength || passwordsMismatch || confirmPassword.length === 0 || passwordMutation.isPending) return; + passwordMutation.mutate(); + }} + className="flex flex-col gap-3 px-7 py-4" + > 修改密码
-
+ ) : ( diff --git a/web/src/lib/api/upload.ts b/web/src/lib/api/upload.ts index 4790fdad..6ec60731 100644 --- a/web/src/lib/api/upload.ts +++ b/web/src/lib/api/upload.ts @@ -36,13 +36,22 @@ const UPLOAD_CTRL_TEXT_CN: Record = { 'not found': '内容不存在或已被删除', }; -export async function uploadFile(file: File, topic?: string): Promise { +/** + * `purpose` only matters when `topic` is omitted (a chat/group-file upload always sends + * `chat_file` regardless) -- currently just the avatar upload path, which used to send no + * `purpose` at all. That omission dated back to this same endpoint's `purpose=user_avatar` 400ing + * (docs/IM-后端问题清单.md 问题28) -- confirmed since fixed (a real PNG upload with that purpose + * now returns 200), so there's no reason left to omit it. + */ +export async function uploadFile(file: File, topic?: string, purpose?: string): Promise { const form = new FormData(); form.append('file', file); form.set('id', getTinode().getNextUniqueId()); if (topic) { form.append('purpose', 'chat_file'); form.append('topic', topic); + } else if (purpose) { + form.append('purpose', purpose); } const res = await fetch(`${fileBase()}/u/`, { method: 'POST',