feat(profile): avatar presets, upload, nickname editing

- FastAPI: preset assets 01–08, GET list/static, PUT /me/avatar/preset,
  safer uploaded-avatar path validation, preset_avatars + HTTP tests.
- Expo: personal-info (library + presets), profile tab avatar,
  resolveApiMediaUrl, auth hooks cache sync, Web multipart helper,
  partial-save messaging + profile i18n.
- Includes existing edits to conversation screen and voice use-player.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-06 13:51:43 +08:00
parent 59d4b19d7d
commit 7ad52fce89
27 changed files with 1271 additions and 270 deletions

View File

@@ -1,4 +1,5 @@
import { router } from 'expo-router';
import { Image } from 'expo-image';
import React from 'react';
import { Pressable, ScrollView, View } from 'react-native';
import { useTranslation } from 'react-i18next';
@@ -21,6 +22,7 @@ import {
import { Icon } from '@/components/ui/icon';
import { Switch } from '@/components/ui/switch';
import { Text } from '@/components/ui/text';
import { resolveApiMediaUrl } from '@/core/api/media-url';
import { useAppSettings } from '@/hooks/use-app-settings';
import { useSession, useLogout } from '@/features/auth/hooks';
import { useCurrentPlan } from '@/features/profile/hooks';
@@ -181,6 +183,8 @@ export default function ProfileScreen() {
themeOptions.find((o) => o.value === themeName)?.label ??
tApp('theme.default');
const avatarUri = resolveApiMediaUrl(user?.avatar_url ?? null);
return (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
@@ -191,10 +195,19 @@ export default function ProfileScreen() {
<View className="items-center gap-4 py-8">
<View className="relative">
<View
className="h-24 w-24 items-center justify-center rounded-full bg-muted"
className="h-24 w-24 items-center justify-center overflow-hidden rounded-full bg-muted"
style={{ borderCurve: 'continuous' }}
>
<Icon as={User} className="text-muted-foreground" size={40} />
{avatarUri ? (
<Image
source={{ uri: avatarUri }}
style={{ width: '100%', height: '100%' }}
contentFit="cover"
accessibilityIgnoresInvertColors
/>
) : (
<Icon as={User} className="text-muted-foreground" size={40} />
)}
</View>
<Pressable
className="absolute bottom-0 right-0 h-9 w-9 items-center justify-center rounded-full border-2 border-background bg-primary"