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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"avatarPresetFailed": "设置预设头像失败",
|
||||
"avatarUploadFailed": "上传头像失败",
|
||||
"cancel": "取消",
|
||||
"tapAwayToClose": "点击空白处关闭",
|
||||
"birthPlacePlaceholder": "出生地",
|
||||
"birthYearPlaceholder": "出生年份",
|
||||
"changeAvatar": "更换头像",
|
||||
|
||||
Reference in New Issue
Block a user