feat(editor): implement deferred image uploading and pending state management
Some checks failed
Frontend CI / ota-ios (push) Successful in 2m3s
Frontend CI / ota-android (push) Successful in 2m4s
Frontend CI / build-and-push-web (push) Failing after 33m21s
Frontend CI / build-android-apk (push) Successful in 59m9s

Refactor the image handling workflow to decouple image selection from the upload process. This improves user experience by allowing users to continue composing posts or trades while images are queued for upload, and increases reliability by using a centralized pending image utility.

- **Deferred Uploads**: Replaced immediate image uploads in `CreatePostScreen`, `PostDetailScreen`, and `CreateTradeScreen` with a "pending" state. Images are now uploaded in bulk during the final submission phase.
- **BlockEditor Enhancements**:
    - Updated `useBlockEditor` to support asynchronous batch uploading of all pending images via a new `uploadPendingImages` method.
    - Removed inline uploading overlays in favor of a more robust state-driven approach.
    - Improved `BlockEditorHandle` to expose block retrieval and batch upload capabilities.
- **API Layer Improvements**:
    - Migrated native image uploads from standard `FormData` to `expo-file-system`'s multipart upload to resolve compatibility issues with recent React Native versions.
    - Maintained `fetch` + `Blob` logic for web platform compatibility.
- **New Utilities**: Introduced `src/utils/pendingImages.ts` to manage the lifecycle of local (pending) vs. remote (uploaded) images across the application.
This commit is contained in:
2026-06-07 00:40:35 +08:00
parent 5c81795d39
commit b15e0c0b0b
11 changed files with 331 additions and 313 deletions

View File

@@ -38,15 +38,26 @@ import {
import { Text, ResponsiveContainer } from '../../components/common';
import { channelService, postService, showPrompt, voteService } from '../../services';
import { ApiError } from '@/services/core';
import { uploadService } from '@/services/upload';
import VoteEditor from '../../components/business/VoteEditor';
import PostMentionInput from '../../components/business/PostMentionInput';
import BlockEditor, { BlockEditorHandle } from '../../components/business/BlockEditor';
import { segmentsToBlocks } from '../../components/business/BlockEditor/blocksToSegments';
import type { EditorBlock } from '../../components/business/BlockEditor/blockEditorTypes';
import {
generateBlockId,
type EditorBlock,
type ImageBlock,
} from '../../components/business/BlockEditor/blockEditorTypes';
import { useResponsive, useResponsiveValue } from '../../hooks';
import * as hrefs from '../../navigation/hrefs';
import { MessageSegment } from '../../types';
import {
type PendingOrRemoteImage,
type RemoteImage,
generatePendingImageId,
makePendingImageFromAsset,
getImageDisplayUri,
uploadAllPendingImages,
} from '../../utils/pendingImages';
// Props 接口
interface CreatePostScreenProps {
@@ -130,7 +141,8 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [segments, setSegments] = useState<MessageSegment[]>([]);
const [images, setImages] = useState<{ uri: string; uploading?: boolean }[]>([]);
// 图片状态pending 表示本地待上传remote 表示已是服务器 URL编辑模式下的已存在图片
const [images, setImages] = useState<PendingOrRemoteImage[]>([]);
const [channelOptions, setChannelOptions] = useState<ChannelOption[]>([]);
const [selectedChannelId, setSelectedChannelId] = useState<string | null>(null);
const [showChannelPicker, setShowChannelPicker] = useState(false);
@@ -213,7 +225,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
}
setTitle(existingPost.title || '');
setContent(existingPost.content || '');
setImages((existingPost.images || []).map((img) => ({ uri: img.url, uploading: false })));
setImages((existingPost.images || []).map((img): RemoteImage => ({ id: generatePendingImageId(), kind: 'remote', url: img.url })));
// 自动判断长文模式segments 中包含图片块即为长文帖子
const hasImageSegments = (existingPost.segments || []).some(s => s.type === 'image');
@@ -233,7 +245,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
loadPostForEdit();
}, [isEditMode, editPostID, router]);
// 选择图片(普通模式)
// 选择图片(普通模式)—— 仅选择,不立即上传
const handlePickImage = async () => {
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
@@ -250,35 +262,12 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
});
if (!result.canceled && result.assets) {
const newImages = result.assets.map(asset => ({ uri: asset.uri, uploading: true }));
const newImages = result.assets.map(asset => makePendingImageFromAsset(asset));
setImages(prev => [...prev, ...newImages]);
// 上传图片
for (let i = 0; i < newImages.length; i++) {
const asset = result.assets[i];
const uploadResult = await uploadService.uploadImage({
uri: asset.uri,
type: asset.mimeType || 'image/jpeg',
});
if (uploadResult) {
setImages(prev => {
const updated = [...prev];
const index = prev.findIndex(img => img.uri === asset.uri);
if (index !== -1) {
updated[index] = { uri: uploadResult.url, uploading: false };
}
return updated;
});
} else {
Alert.alert('上传失败', '图片上传失败,请重试');
setImages(prev => prev.filter(img => img.uri !== asset.uri));
}
}
}
};
// 拍照
// 拍照 —— 仅添加,不立即上传
const handleTakePhoto = async () => {
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
@@ -294,33 +283,13 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
if (!result.canceled && result.assets[0]) {
const asset = result.assets[0];
setImages(prev => [...prev, { uri: asset.uri, uploading: true }]);
// 上传图片
const uploadResult = await uploadService.uploadImage({
uri: asset.uri,
type: asset.mimeType || 'image/jpeg',
});
if (uploadResult) {
setImages(prev => {
const updated = [...prev];
const index = prev.findIndex(img => img.uri === asset.uri);
if (index !== -1) {
updated[index] = { uri: uploadResult.url, uploading: false };
}
return updated;
});
} else {
Alert.alert('上传失败', '图片上传失败,请重试');
setImages(prev => prev.filter(img => img.uri !== asset.uri));
}
setImages(prev => [...prev, makePendingImageFromAsset(asset)]);
}
};
// 删除图片
const handleRemoveImage = (index: number) => {
setImages(images.filter((_, i) => i !== index));
setImages(prev => prev.filter((_, i) => i !== index));
};
// 选择频道(单选,可再次点击取消)
@@ -350,26 +319,44 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
// 切换长文/非长文模式时保留数据
const handleToggleLongPostMode = useCallback(() => {
if (!isLongPostMode) {
// 非长文 -> 长文:将当前文本和图片转为 blocks
const segs: MessageSegment[] = [];
// 非长文 -> 长文:将当前文本和图片(包括 pending转为 blocks
const blocks: EditorBlock[] = [];
if (content.trim()) {
segs.push({ type: 'text', data: { text: content } });
blocks.push({
id: generateBlockId(),
type: 'text',
text: content,
activeMentions: new Map(),
});
}
for (const img of images) {
if (img.uri && !img.uploading) {
segs.push({ type: 'image', data: { url: img.uri } });
const imageBlock: ImageBlock = img.kind === 'remote'
? { id: generateBlockId(), type: 'image', localUri: img.url, remoteUrl: img.url }
: { id: generateBlockId(), type: 'image', localUri: img.localUri, mimeType: img.mimeType };
blocks.push(imageBlock);
}
setInitialBlocks(blocks.length > 0 ? blocks : undefined);
} else {
// 长文 -> 非长文:从 BlockEditor 提取文本和所有图片(包括 pending
const editorContent = blockEditorRef.current?.getContent() || '';
const editorBlocks = blockEditorRef.current?.getBlocks() || [];
if (editorContent) setContent(editorContent);
const editorImages: PendingOrRemoteImage[] = [];
for (const block of editorBlocks) {
if (block.type !== 'image') continue;
if (block.remoteUrl) {
editorImages.push({ id: generatePendingImageId(), kind: 'remote', url: block.remoteUrl });
} else {
editorImages.push({
id: generatePendingImageId(),
kind: 'pending',
localUri: block.localUri,
mimeType: block.mimeType,
});
}
}
setInitialBlocks(segs.length > 0 ? segmentsToBlocks(segs) : undefined);
} else {
// 长文 -> 非长文:从 BlockEditor 提取文本和图片
const editorContent = blockEditorRef.current?.getContent() || '';
const editorSegments = blockEditorRef.current?.getSegments() || [];
if (editorContent) setContent(editorContent);
const editorImages = editorSegments
.filter((s): s is Extract<typeof s, { type: 'image' }> => s.type === 'image')
.map(s => ({ uri: s.data.url, uploading: false }));
if (editorImages.length > 0) setImages(editorImages);
setImages(editorImages);
}
setIsLongPostMode(prev => !prev);
}, [isLongPostMode, content, images]);
@@ -436,25 +423,37 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
return;
}
// 检查是否有图片正在上传
if (isLongPostMode) {
if (blockEditorRef.current?.hasUploadingImages()) {
Alert.alert('请稍候', '图片正在上传中,请稍后再试');
return;
}
} else {
const uploadingImages = images.filter(img => img.uploading);
if (uploadingImages.length > 0) {
Alert.alert('请稍候', '图片正在上传中,请稍后再试');
return;
}
}
// 检查 BlockEditor 中是否有正在上传的图片
// (已无此状态,发布时会统一上传,无需检查)
setPosting(true);
try {
// 普通模式下,发布前上传所有待上传的图片
let uploadedImageUrls: string[] = [];
if (!isLongPostMode && images.length > 0) {
const uploadResult = await uploadAllPendingImages(images, (id, url) => {
setImages(prev => prev.map(i => i.id === id ? { id, kind: 'remote' as const, url } : i));
});
if (!uploadResult.success) {
setPosting(false);
Alert.alert('上传失败', '部分图片上传失败,请重试');
return;
}
uploadedImageUrls = uploadResult.urls;
}
// 长文模式下,发布前上传 BlockEditor 中所有 pending 图片
if (isLongPostMode) {
const uploadOk = await blockEditorRef.current?.uploadPendingImages();
if (uploadOk === false) {
setPosting(false);
Alert.alert('上传失败', '部分图片上传失败,请重试');
return;
}
}
if (isVotePost) {
const imageUrls = images.map(img => img.uri);
// 验证投票选项
const validOptions = voteOptions.filter(opt => opt.trim() !== '');
if (validOptions.length < 2) {
@@ -467,7 +466,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
await voteService.createVotePost({
title: title.trim(),
content: content.trim(),
images: imageUrls.length > 0 ? imageUrls : undefined,
images: uploadedImageUrls.length > 0 ? uploadedImageUrls : undefined,
channel_id: selectedChannelId || undefined,
vote_options: validOptions.map(opt => ({ content: opt })),
});
@@ -506,13 +505,12 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
}
} else {
// 普通模式
const imageUrls = images.map(img => img.uri);
if (isEditMode && editPostID) {
const updated = await postService.updatePost(editPostID, {
title: title.trim(),
content: content.trim(),
segments: segments.some(s => s.type !== 'text') ? segments : undefined,
images: imageUrls.length > 0 ? imageUrls : undefined,
images: uploadedImageUrls.length > 0 ? uploadedImageUrls : undefined,
});
if (!updated) {
throw new Error('更新帖子失败');
@@ -524,7 +522,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
title: title.trim(),
content: content.trim(),
segments: segments.some(s => s.type !== 'text') ? segments : undefined,
images: imageUrls.length > 0 ? imageUrls : undefined,
images: uploadedImageUrls.length > 0 ? uploadedImageUrls : undefined,
channel_id: selectedChannelId || undefined,
});
showPrompt({ type: 'info', title: '审核中', message: '帖子已提交,内容审核中,稍后展示', duration: 2600 });
@@ -547,27 +545,25 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
<View style={styles.imageGrid}>
{images.map((img, index) => (
<Animated.View
key={`${img.uri}-${index}`}
key={img.id}
style={[
styles.imageGridItem,
{
width: imageSize,
{
width: imageSize,
height: imageSize,
opacity: fadeAnim,
transform: [{ scale: fadeAnim }]
opacity: fadeAnim,
transform: [{ scale: fadeAnim }]
}
]}
>
<Image source={{ uri: img.uri }} style={styles.gridImage} resizeMode="cover" />
{img.uploading && (
<View style={styles.uploadingOverlay}>
<MaterialCommunityIcons name="loading" size={20} color={colors.text.inverse} />
</View>
)}
<Image
source={{ uri: getImageDisplayUri(img) }}
style={styles.gridImage}
resizeMode="cover"
/>
<TouchableOpacity
style={styles.removeImageButton}
onPress={() => handleRemoveImage(index)}
disabled={img.uploading}
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
>
<View style={styles.removeImageButtonInner}>
@@ -1044,12 +1040,6 @@ function createCreatePostStyles(colors: AppColors) {
width: '100%',
height: '100%',
},
uploadingOverlay: {
...StyleSheet.absoluteFill,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
removeImageButton: {
position: 'absolute',
top: 4,

View File

@@ -38,12 +38,18 @@ import {
import { Post, Comment, VoteResultDTO, VoteOptionDTO, MessageSegment } from '../../types';
import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/auth';
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
import { postService, commentService, authService, showPrompt, voteService } from '../../services';
import { postSyncService } from '@/services/post';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import { CommentItem, VoteCard, ReportDialog, ShareSheet, PostContentRenderer, PostMentionInput } from '../../components/business';
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout, AppBackButton } from '../../components/common';
import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../hooks';
import {
type PendingOrRemoteImage,
makePendingImageFromAsset,
getImageDisplayUri,
uploadAllPendingImages,
} from '../../utils/pendingImages';
import { handleError } from '@/services/core';
import { formatDateTime as formatDateTimeUtil, formatRelativeTime as formatRelativeTimeUtil } from '@/utils/formatTime';
import * as hrefs from '../../navigation/hrefs';
@@ -251,7 +257,7 @@ export const PostDetailScreen: React.FC = () => {
const [keyboardHeight, setKeyboardHeight] = useState(0);
const [isDeleting, setIsDeleting] = useState(false);
// 评论图片相关状态
const [commentImages, setCommentImages] = useState<{ uri: string; uploading?: boolean }[]>([]);
const [commentImages, setCommentImages] = useState<PendingOrRemoteImage[]>([]);
const [showEmojiPanel, setShowEmojiPanel] = useState(false);
const [isFollowing, setIsFollowing] = useState(false);
const [isFollowingMe, setIsFollowingMe] = useState(false);
@@ -688,31 +694,8 @@ export const PostDetailScreen: React.FC = () => {
});
if (!result.canceled && result.assets) {
const newImages = result.assets.map(asset => ({ uri: asset.uri, uploading: true }));
const newImages = result.assets.map(asset => makePendingImageFromAsset(asset, 'ci'));
setCommentImages(prev => [...prev, ...newImages]);
// 上传图片
for (let i = 0; i < newImages.length; i++) {
const asset = result.assets[i];
const uploadResult = await uploadService.uploadImage({
uri: asset.uri,
type: asset.mimeType || 'image/jpeg',
});
if (uploadResult) {
setCommentImages(prev => {
const updated = [...prev];
const index = prev.findIndex(img => img.uri === asset.uri);
if (index !== -1) {
updated[index] = { uri: uploadResult.url, uploading: false };
}
return updated;
});
} else {
Alert.alert('上传失败', '图片上传失败,请重试');
setCommentImages(prev => prev.filter(img => img.uri !== asset.uri));
}
}
}
};
@@ -727,11 +710,17 @@ export const PostDetailScreen: React.FC = () => {
const hasContent = commentText.trim() || commentImages.length > 0;
if (!hasContent || !post) return;
// 检查是否有图片正在上传
const uploadingImages = commentImages.filter(img => img.uploading);
if (uploadingImages.length > 0) {
Alert.alert('请稍候', '图片正在上传中,请稍后再试');
return;
// 上传所有 pending 图片
let uploadedImageUrls: string[] = [];
if (commentImages.length > 0) {
const uploadResult = await uploadAllPendingImages(commentImages, (id, url) => {
setCommentImages(prev => prev.map(i => i.id === id ? { id, kind: 'remote' as const, url } : i));
});
if (!uploadResult.success) {
Alert.alert('上传失败', '部分图片上传失败,请重试');
return;
}
uploadedImageUrls = uploadResult.urls;
}
const tempId = `new-${Date.now()}`;
@@ -741,9 +730,6 @@ export const PostDetailScreen: React.FC = () => {
// 获取根评论 ID如果被回复评论有 root_id则使用 root_id否则使用 parent_id即被回复的是顶级评论
const rootId = isReply ? (replyingTo.root_id || replyingTo.id) : null;
// 获取已上传图片的URL列表
const uploadedImageUrls = commentImages.filter(img => !img.uploading).map(img => img.uri);
// 先创建临时评论显示在列表中
const tempComment: Comment = {
id: tempId,
@@ -1501,13 +1487,12 @@ export const PostDetailScreen: React.FC = () => {
{commentImages.length > 0 && (
<ScrollView horizontal style={styles.commentImagesPreview} showsHorizontalScrollIndicator={false}>
{commentImages.map((image, index) => (
<View key={index} style={styles.commentImageWrapper}>
<Image source={{ uri: image.uri }} style={styles.commentImageThumbnail} resizeMode="cover" />
{image.uploading && (
<View style={styles.uploadingOverlay}>
<Loading size="sm" />
</View>
)}
<View key={image.id} style={styles.commentImageWrapper}>
<Image
source={{ uri: getImageDisplayUri(image) }}
style={styles.commentImageThumbnail}
resizeMode="cover"
/>
<TouchableOpacity
style={styles.removeImageButton}
onPress={() => handleRemoveCommentImage(index)}
@@ -2185,17 +2170,6 @@ function createPostDetailStyles(colors: AppColors) {
height: 60,
borderRadius: borderRadius.sm,
},
uploadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)',
justifyContent: 'center',
alignItems: 'center',
borderRadius: borderRadius.sm,
},
removeImageButton: {
position: 'absolute',
top: -6,

View File

@@ -24,15 +24,21 @@ import {
ActivityIndicator,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { Loading } from '../../components/common';
import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common';
import { tradeService } from '../../services/trade/tradeService';
import { uploadService } from '../../services';
import type { TradeType, TradeCategory, TradeCondition, TradeItemDetailDTO } from '../../types/trade';
import { ALL_TRADE_CATEGORIES, ALL_TRADE_CONDITIONS } from '../../types/trade';
import {
type PendingOrRemoteImage,
type RemoteImage,
generatePendingImageId,
makePendingImageFromAsset,
getImageDisplayUri,
uploadAllPendingImages,
} from '../../utils/pendingImages';
interface CreateTradeScreenProps {
onClose: () => void;
@@ -258,13 +264,6 @@ function createStyles(colors: AppColors) {
justifyContent: 'center',
backgroundColor: colors.background.default,
},
uploadingOverlay: {
...StyleSheet.absoluteFill,
backgroundColor: 'rgba(0,0,0,0.3)',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 14,
},
// 提交按钮
submitBtn: {
@@ -323,8 +322,8 @@ export function CreateTradeScreen({ onClose, onSuccess, editItem }: CreateTradeS
const [condition, setCondition] = useState<TradeCondition | undefined>(
editItem?.condition as TradeCondition | undefined
);
const [images, setImages] = useState<{ uri: string; uploading: boolean }[]>(
editItem?.images?.map(img => ({ uri: img.url, uploading: false })) || []
const [images, setImages] = useState<PendingOrRemoteImage[]>(
editItem?.images?.map((img): RemoteImage => ({ id: generatePendingImageId('ti'), kind: 'remote', url: img.url })) || []
);
const [submitting, setSubmitting] = useState(false);
@@ -352,19 +351,29 @@ export function CreateTradeScreen({ onClose, onSuccess, editItem }: CreateTradeS
Alert.alert('提示', '请输入标题');
return;
}
if (images.some(img => img.uploading)) {
Alert.alert('提示', '请等待图片上传完成');
return;
}
setSubmitting(true);
try {
// 上传所有 pending 图片
let uploadedImageUrls: string[] = [];
if (images.length > 0) {
const uploadResult = await uploadAllPendingImages(images, (id, url) => {
setImages(prev => prev.map(i => i.id === id ? { id, kind: 'remote' as const, url } : i));
});
if (!uploadResult.success) {
setSubmitting(false);
Alert.alert('上传失败', '部分图片上传失败,请重试');
return;
}
uploadedImageUrls = uploadResult.urls;
}
const payload = {
trade_type: tradeType,
category,
title: title.trim(),
content: content.trim() || title.trim(),
images: images.map(img => img.uri),
images: uploadedImageUrls,
price: price ? parseFloat(price) : null,
condition,
};
@@ -403,37 +412,16 @@ export function CreateTradeScreen({ onClose, onSuccess, editItem }: CreateTradeS
});
if (!result.canceled && result.assets) {
const newImages = result.assets.map(asset => ({ uri: asset.uri, uploading: true }));
const newImages = result.assets.map(asset => makePendingImageFromAsset(asset, 'ti'));
setImages(prev => [...prev, ...newImages]);
for (let i = 0; i < newImages.length; i++) {
const asset = result.assets[i];
const uploadResult = await uploadService.uploadImage({
uri: asset.uri,
type: asset.mimeType || 'image/jpeg',
});
if (uploadResult) {
setImages(prev => {
const updated = [...prev];
const index = prev.findIndex(img => img.uri === asset.uri);
if (index !== -1) {
updated[index] = { uri: uploadResult.url, uploading: false };
}
return updated;
});
} else {
setImages(prev => prev.filter(img => img.uri !== asset.uri));
}
}
}
}, [images.length]);
const removeImage = useCallback((uri: string) => {
setImages(prev => prev.filter(img => img.uri !== uri));
const removeImage = useCallback((id: string) => {
setImages(prev => prev.filter(img => img.id !== id));
}, []);
const canSubmit = title.trim().length > 0 && !submitting && !images.some(i => i.uploading);
const canSubmit = title.trim().length > 0 && !submitting;
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
@@ -558,14 +546,9 @@ export function CreateTradeScreen({ onClose, onSuccess, editItem }: CreateTradeS
<Text style={styles.inputLabel}> ({images.length}/9)</Text>
<View style={styles.imageGrid}>
{images.map((img) => (
<View key={img.uri} style={styles.imageItem}>
<Image source={{ uri: img.uri.startsWith('http') ? img.uri : img.uri }} style={styles.imagePreview} />
{img.uploading && (
<View style={styles.uploadingOverlay}>
<Loading size="sm" />
</View>
)}
<Pressable style={styles.removeImageBtn} onPress={() => removeImage(img.uri)}>
<View key={img.id} style={styles.imageItem}>
<Image source={{ uri: getImageDisplayUri(img) }} style={styles.imagePreview} />
<Pressable style={styles.removeImageBtn} onPress={() => removeImage(img.id)}>
<MaterialCommunityIcons name="close" size={16} color="#fff" />
</Pressable>
</View>