chore: resolve WIP after merging internal/development

- .gitignore: keep api/uploads ignore and copyright_source_listing.pdf path

- auth: keep COS avatar upload URL; delete prior COS object when applying preset

- i18n: regenerate resources.ts (includes profile tapAwayToClose)

- Avatar/COS tests and personal-info remain from prior local work

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-18 15:34:50 +08:00
parent 98802240ac
commit eabda2c6a9
12 changed files with 350 additions and 97 deletions

View File

@@ -26,6 +26,7 @@ import { Text } from '@/components/ui/text';
import { ScreenHeader } from '@/components/screen-header';
import { resolveApiMediaUrl } from '@/core/api/media-url';
import { ApiError } from '@/core/api/types';
import { cn } from '@/lib/utils';
import { buildAvatarUploadFormData } from '@/features/auth/avatar-upload-form-data';
import {
useAvatarPresets,
@@ -131,6 +132,7 @@ export default function PersonalInfoScreen() {
const avatarBusy = uploadAvatar.isPending || setPreset.isPending;
const avatarUri = resolveApiMediaUrl(profile?.avatar_url ?? null);
const tileSize = computePresetTileSize();
const avatarPresetSheetMaxH = Dimensions.get('window').height * 0.88;
const handleSave = async () => {
const trimmed = nickname.trim();
@@ -288,92 +290,151 @@ export default function PersonalInfoScreen() {
<Modal
visible={avatarModalOpen}
animationType="slide"
presentationStyle="pageSheet"
transparent
statusBarTranslucent={Platform.OS === 'android'}
onRequestClose={closeAvatarModal}
>
<SafeAreaView className="flex-1 bg-background">
<View className="flex-row items-center justify-between border-b border-border px-3 py-2">
{avatarStep === 'presets' ? (
<Pressable
accessibilityRole="button"
hitSlop={12}
onPress={() => setAvatarStep('menu')}
>
<Text className="text-primary">{t('personalInfo.back')}</Text>
</Pressable>
) : (
<View className="w-14" />
)}
<Text variant="large">
{avatarStep === 'presets'
? t('personalInfo.presetPickTitle')
: t('personalInfo.changeAvatar')}
</Text>
<Pressable
accessibilityRole="button"
hitSlop={12}
onPress={closeAvatarModal}
<View className="flex-1 justify-end">
<Pressable
accessibilityRole="button"
accessibilityLabel={t('personalInfo.tapAwayToClose')}
className="absolute inset-0 bg-black/50"
onPress={closeAvatarModal}
/>
<View
className="w-full overflow-hidden rounded-t-2xl border-t border-border bg-background"
style={
avatarStep === 'presets'
? { maxHeight: avatarPresetSheetMaxH }
: undefined
}
>
<SafeAreaView
edges={['bottom', 'left', 'right']}
style={
avatarStep === 'presets'
? { flex: 1, minHeight: 0 }
: undefined
}
>
<Text className="text-primary">{t('personalInfo.cancel')}</Text>
</Pressable>
</View>
{avatarStep === 'menu' ? (
<View className="gap-3 px-4 pt-6">
<Button variant="outline" onPress={() => void pickFromLibrary()}>
<Text>{t('personalInfo.chooseFromLibrary')}</Text>
</Button>
<Button
variant="outline"
onPress={() => setAvatarStep('presets')}
<View
className="flex-row items-center border-b border-border px-4 pb-3"
style={{
paddingTop:
avatarStep === 'presets'
? Math.max(insets.top, 12)
: 12,
}}
>
<Text>{t('personalInfo.choosePreset')}</Text>
</Button>
</View>
) : (
<View className="flex-1 px-4 pt-4">
{presetsLoading ? (
<ActivityIndicator style={{ marginTop: 32 }} />
<View className="min-w-[88px] flex-row items-center justify-start">
{avatarStep === 'presets' ? (
<Pressable
accessibilityRole="button"
hitSlop={{ top: 12, bottom: 12, left: 8, right: 8 }}
onPress={() => setAvatarStep('menu')}
>
<Text className="text-primary">
{t('personalInfo.back')}
</Text>
</Pressable>
) : null}
</View>
<View className="min-w-0 flex-1 items-center justify-center px-2">
<Text variant="large" numberOfLines={1}>
{avatarStep === 'presets'
? t('personalInfo.presetPickTitle')
: t('personalInfo.changeAvatar')}
</Text>
</View>
<View className="min-w-[88px] flex-row items-center justify-end">
<Pressable
accessibilityRole="button"
hitSlop={{ top: 12, bottom: 12, left: 8, right: 8 }}
onPress={closeAvatarModal}
>
<Text className="text-primary">
{t('personalInfo.cancel')}
</Text>
</Pressable>
</View>
</View>
{avatarStep === 'menu' ? (
<View className="gap-1.5 px-4 pb-4 pt-3">
<Pressable
accessibilityRole="button"
disabled={avatarBusy}
onPress={() => void pickFromLibrary()}
className={cn(
'items-center rounded-md border border-border bg-background py-1 px-3 active:bg-accent',
avatarBusy && 'opacity-50',
)}
>
<Text className="text-sm font-medium leading-5 text-foreground">
{t('personalInfo.chooseFromLibrary')}
</Text>
</Pressable>
<Pressable
accessibilityRole="button"
disabled={avatarBusy}
onPress={() => setAvatarStep('presets')}
className={cn(
'items-center rounded-md border border-border bg-background py-1 px-3 active:bg-accent',
avatarBusy && 'opacity-50',
)}
>
<Text className="text-sm font-medium leading-5 text-foreground">
{t('personalInfo.choosePreset')}
</Text>
</Pressable>
</View>
) : (
<ScrollView
keyboardShouldPersistTaps="handled"
contentContainerStyle={{
flexDirection: 'row',
flexWrap: 'wrap',
gap: TILE_GAP,
paddingBottom: 24,
}}
>
{(presets ?? []).map((item) => {
const uri = resolveApiMediaUrl(item.url);
return (
<Pressable
key={item.id}
accessibilityRole="button"
accessibilityLabel={`preset-${item.id}`}
disabled={avatarBusy}
className="overflow-hidden rounded-xl bg-muted"
onPress={() => void applyPreset(item.id)}
style={{
width: tileSize,
height: tileSize,
}}
>
{uri ? (
<Image
source={{ uri }}
style={{ width: '100%', height: '100%' }}
contentFit="cover"
/>
) : null}
</Pressable>
);
})}
</ScrollView>
<View className="min-h-0 flex-1 px-4 pb-2 pt-4">
{presetsLoading ? (
<ActivityIndicator style={{ marginTop: 32 }} />
) : (
<ScrollView
keyboardShouldPersistTaps="handled"
className="min-h-0 flex-1"
contentContainerStyle={{
flexDirection: 'row',
flexWrap: 'wrap',
gap: TILE_GAP,
paddingBottom: 24,
}}
>
{(presets ?? []).map((item) => {
const uri = resolveApiMediaUrl(item.url);
return (
<Pressable
key={item.id}
accessibilityRole="button"
accessibilityLabel={`preset-${item.id}`}
disabled={avatarBusy}
className="overflow-hidden rounded-xl bg-muted"
onPress={() => void applyPreset(item.id)}
style={{
width: tileSize,
height: tileSize,
}}
>
{uri ? (
<Image
source={{ uri }}
style={{ width: '100%', height: '100%' }}
contentFit="cover"
/>
) : null}
</Pressable>
);
})}
</ScrollView>
)}
</View>
)}
</View>
)}
</SafeAreaView>
</SafeAreaView>
</View>
</View>
</Modal>
</View>
);

View File

@@ -233,6 +233,7 @@ interface Resources {
"savePartialBody": "Your nickname was saved, but profile fields below could not be saved. Check your connection and tap Save again.",
"savePartialTitle": "Partially saved",
"saving": "Saving…",
"tapAwayToClose": "Tap outside to close",
"title": "Personal info"
},
"signOut": "Sign Out",

View File

@@ -37,6 +37,7 @@
"avatarPresetFailed": "Could not set preset avatar",
"avatarUploadFailed": "Could not upload avatar",
"cancel": "Cancel",
"tapAwayToClose": "Tap outside to close",
"birthPlacePlaceholder": "Birthplace",
"birthYearPlaceholder": "Birth year",
"changeAvatar": "Change photo",

View File

@@ -37,6 +37,7 @@
"avatarPresetFailed": "设置预设头像失败",
"avatarUploadFailed": "上传头像失败",
"cancel": "取消",
"tapAwayToClose": "点击空白处关闭",
"birthPlacePlaceholder": "出生地",
"birthYearPlaceholder": "出生年份",
"changeAvatar": "更换头像",