feat(ChatScreen): enhance message input and attachment handling
Some checks failed
Frontend CI / build-and-push-web (push) Successful in 8m3s
Frontend CI / ota-android (push) Successful in 11m1s
Frontend CI / build-android-apk (push) Has been cancelled

- Updated ChatInput to manage pending attachments, allowing users to upload multiple images before sending.
- Introduced functionality to remove pending attachments and display their thumbnails in the input area.
- Enhanced ChatScreen to reflect changes in attachment handling, including dynamic messaging during uploads.
- Refactored message segment rendering to support inline text and grouped images, improving visual organization.
- Added constants for maximum pending images to enforce limits on user uploads.
This commit is contained in:
lafay
2026-03-25 03:44:16 +08:00
parent 583ac64dfd
commit dc8f9061ab
7 changed files with 338 additions and 95 deletions

View File

@@ -39,8 +39,9 @@ import {
SenderInfo,
ChatRouteParams,
MenuPosition,
PendingChatAttachment,
} from './types';
import { TIME_SEPARATOR_INTERVAL, MEMBERS_PAGE_SIZE } from './constants';
import { TIME_SEPARATOR_INTERVAL, MEMBERS_PAGE_SIZE, MAX_PENDING_CHAT_IMAGES } from './constants';
import {
deleteMessage as deleteMessageFromDb,
clearConversationMessages,
@@ -120,6 +121,8 @@ export const useChatScreen = () => {
const [otherUserLastReadSeq, setOtherUserLastReadSeq] = useState<number>(0);
const [activePanel, setActivePanel] = useState<PanelType>('none');
const [sendingImage, setSendingImage] = useState(false);
const [uploadingAttachments, setUploadingAttachments] = useState(false);
const [pendingAttachments, setPendingAttachments] = useState<PendingChatAttachment[]>([]);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMoreHistory, setHasMoreHistory] = useState(true);
const flatListRef = useRef<any>(null);
@@ -305,6 +308,19 @@ export const useChatScreen = () => {
historyLoadingLockUntilRef.current = 0;
}, [conversationId]);
useEffect(() => {
setPendingAttachments([]);
}, [conversationId]);
const removePendingAttachment = useCallback((id: string) => {
setPendingAttachments(prev => prev.filter(p => p.id !== id));
}, []);
const isComposerBusy = useMemo(
() => sending || sendingImage || uploadingAttachments,
[sending, sendingImage, uploadingAttachments]
);
const isHistoryLoadingLocked = useCallback(() => {
return Date.now() < historyLoadingLockUntilRef.current;
}, []);
@@ -833,10 +849,11 @@ export const useChatScreen = () => {
return segments;
}, []);
// 【新架构】发送消息
// 【新架构】发送消息(支持纯文字、纯多图、图文同条)
const handleSend = useCallback(async () => {
const trimmedText = inputText.trim();
if (!trimmedText || !conversationId) return;
const hasPending = pendingAttachments.length > 0;
if ((!trimmedText && !hasPending) || !conversationId) return;
if (isGroupChat && isMuted) {
if (muteAll) {
@@ -847,15 +864,61 @@ export const useChatScreen = () => {
return;
}
if (!isGroupChat && !canSendFirstPrivateText) {
Alert.alert('无法发送', '对方未关注你前,仅允许发送一条消息');
return;
if (!isGroupChat) {
if (trimmedText && !canSendFirstPrivateText) {
Alert.alert('无法发送', '对方未关注你前,仅允许发送一条消息');
return;
}
if (hasPending && !canSendPrivateImage) {
Alert.alert('无法发送', '对方未关注你前,暂不支持发送图片');
return;
}
}
const uploadedUrls: string[] = [];
if (hasPending) {
setUploadingAttachments(true);
try {
const results = await Promise.all(
pendingAttachments.map(p =>
uploadService.uploadChatImage({ uri: p.uri, type: p.mimeType })
)
);
for (let i = 0; i < results.length; i++) {
const r = results[i];
if (!r?.url) {
Alert.alert('上传失败', `${i + 1} 张图片上传失败,请重试`);
return;
}
uploadedUrls.push(r.url);
}
} catch (error) {
console.error('上传图片失败:', error);
Alert.alert('上传失败', getSendErrorMessage(error, '图片上传失败,请重试'));
return;
} finally {
setUploadingAttachments(false);
}
}
setSending(true);
try {
const segments = buildTextSegments(trimmedText, replyingTo);
const segments: MessageSegment[] = [...buildTextSegments(trimmedText, replyingTo)];
for (const url of uploadedUrls) {
segments.push({
type: 'image',
data: {
url,
thumbnail_url: url,
} as ImageSegmentData,
});
}
if (segments.length === 0) {
Alert.alert('无法发送', '消息内容为空');
return;
}
if (isGroupChat && routeGroupId) {
await messageService.sendMessageByAction('group', conversationId, segments);
@@ -864,15 +927,18 @@ export const useChatScreen = () => {
setSelectedMentions([]);
setMentionAll(false);
setReplyingTo(null);
setPendingAttachments([]);
} else {
// 【新架构】私聊消息通过 MessageManager 发送
await sendMessageViaManager(segments);
setInputText('');
setSelectedMentions([]);
setMentionAll(false);
setReplyingTo(null);
setPendingAttachments([]);
}
setTimeout(() => {
scrollToLatest(false, false, 'send-text');
scrollToLatest(false, false, hasPending ? 'send-mixed' : 'send-text');
}, 100);
} catch (error) {
console.error('发送消息失败:', error);
@@ -880,60 +946,31 @@ export const useChatScreen = () => {
} finally {
setSending(false);
}
}, [inputText, conversationId, isGroupChat, routeGroupId, selectedMentions, mentionAll, sendMessageViaManager, isMuted, muteAll, buildTextSegments, replyingTo, getSendErrorMessage, canSendFirstPrivateText, scrollToLatest]);
}, [
inputText,
pendingAttachments,
conversationId,
isGroupChat,
routeGroupId,
sendMessageViaManager,
isMuted,
muteAll,
buildTextSegments,
replyingTo,
getSendErrorMessage,
canSendFirstPrivateText,
canSendPrivateImage,
scrollToLatest,
]);
// 发送图片消息
const handleSendImage = useCallback(async (imageUri: string, mimeType?: string) => {
if (!conversationId) return;
if (isGroupChat && isMuted) {
if (muteAll) {
Alert.alert('无法发送', '当前群组已开启全员禁言');
} else {
Alert.alert('无法发送', '你已被管理员禁言');
}
return;
}
if (!isGroupChat && !canSendPrivateImage) {
Alert.alert('无法发送', '对方未关注你前,暂不支持发送图片');
return;
}
setSendingImage(true);
try {
const uploadResult = await uploadService.uploadChatImage({ uri: imageUri, type: mimeType });
if (!uploadResult) {
Alert.alert('上传失败', '图片上传失败,请重试');
return;
}
const segments = buildImageSegments(uploadResult.url, uploadResult.url, undefined, undefined, replyingTo);
if (isGroupChat && routeGroupId) {
await messageService.sendMessageByAction('group', conversationId, segments);
setReplyingTo(null);
} else {
// 【新架构】私聊图片通过 MessageManager 发送
await sendMessageViaManager(segments);
setReplyingTo(null);
}
setTimeout(() => {
scrollToLatest(false, false, 'send-image');
}, 100);
} catch (error) {
console.error('发送图片失败:', error);
Alert.alert('发送失败', getSendErrorMessage(error, '图片发送失败,请重试'));
} finally {
setSendingImage(false);
}
}, [conversationId, isGroupChat, routeGroupId, sendMessageViaManager, isMuted, muteAll, buildImageSegments, replyingTo, getSendErrorMessage, canSendPrivateImage, scrollToLatest]);
// 选择图片
// 选择图片(多选进入输入框待发送,点发送时与文字一并发出)
const handlePickImage = useCallback(async () => {
if (pendingAttachments.length >= MAX_PENDING_CHAT_IMAGES) {
Alert.alert('提示', `最多添加 ${MAX_PENDING_CHAT_IMAGES} 张图片`);
setActivePanel('none');
return;
}
try {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
@@ -942,16 +979,23 @@ export const useChatScreen = () => {
return;
}
const remaining = MAX_PENDING_CHAT_IMAGES - pendingAttachments.length;
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: 'images',
allowsEditing: false,
quality: 1,
allowsMultipleSelection: false,
allowsMultipleSelection: true,
selectionLimit: remaining,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
const asset = result.assets[0];
await handleSendImage(asset.uri, asset.mimeType ?? undefined);
const newItems: PendingChatAttachment[] = result.assets.map((asset, i) => ({
id: `${Date.now()}-${i}-${Math.random().toString(36).slice(2, 9)}`,
uri: asset.uri,
mimeType: asset.mimeType ?? undefined,
}));
setPendingAttachments(prev => [...prev, ...newItems].slice(0, MAX_PENDING_CHAT_IMAGES));
}
} catch (error) {
console.error('选择图片失败:', error);
@@ -959,10 +1003,16 @@ export const useChatScreen = () => {
}
setActivePanel('none');
}, [handleSendImage]);
}, [pendingAttachments.length]);
// 拍摄照片
// 拍摄照片(加入待发送队列)
const handleTakePhoto = useCallback(async () => {
if (pendingAttachments.length >= MAX_PENDING_CHAT_IMAGES) {
Alert.alert('提示', `最多添加 ${MAX_PENDING_CHAT_IMAGES} 张图片`);
setActivePanel('none');
return;
}
try {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
@@ -979,7 +1029,18 @@ export const useChatScreen = () => {
if (!result.canceled && result.assets && result.assets.length > 0) {
const asset = result.assets[0];
await handleSendImage(asset.uri, asset.mimeType ?? undefined);
setPendingAttachments(prev =>
prev.length >= MAX_PENDING_CHAT_IMAGES
? prev
: [
...prev,
{
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
uri: asset.uri,
mimeType: asset.mimeType ?? undefined,
},
]
);
}
} catch (error) {
console.error('拍摄照片失败:', error);
@@ -987,7 +1048,7 @@ export const useChatScreen = () => {
}
setActivePanel('none');
}, [handleSendImage]);
}, [pendingAttachments.length]);
// 处理更多功能
const handleMoreAction = useCallback((actionId: string) => {
@@ -1266,6 +1327,9 @@ export const useChatScreen = () => {
sending,
activePanel,
sendingImage,
uploadingAttachments,
pendingAttachments,
isComposerBusy,
replyingTo,
longPressMenuVisible,
selectedMessage,
@@ -1301,8 +1365,8 @@ export const useChatScreen = () => {
shouldShowTime,
handleInputChange,
handleSend,
handleSendImage,
handlePickImage,
removePendingAttachment,
handleTakePhoto,
handleMoreAction,
handleInsertEmoji,