feat(ChatScreen): enhance message input and attachment handling
- 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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user