Migrate frontend realtime messaging to SSE.

Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts.

Made-with: Cursor
This commit is contained in:
2026-03-10 12:58:23 +08:00
parent 63e32b15a3
commit be84c01abd
25 changed files with 974 additions and 1305 deletions

View File

@@ -117,8 +117,8 @@ const GroupInfoScreen: React.FC = () => {
// 并行加载群组信息和成员列表
const [groupData, membersData] = await Promise.all([
groupManager.getGroup(groupId),
groupManager.getMembers(groupId, 1, 100),
groupManager.getGroup(groupId, true),
groupManager.getMembers(groupId, 1, 100, true),
]);
setGroup(groupData);

View File

@@ -92,11 +92,16 @@ const GroupMembersScreen: React.FC = () => {
const isAdmin = currentMember?.role === 'admin' || isOwner;
// 加载成员列表
const loadMembers = useCallback(async (pageNum: number = 1, refresh: boolean = false) => {
const loadMembers = useCallback(
async (
pageNum: number = 1,
refresh: boolean = false,
forceRefresh: boolean = false
) => {
if (!hasMore && !refresh) return;
try {
const response = await groupManager.getMembers(groupId, pageNum, 50);
const response = await groupManager.getMembers(groupId, pageNum, 50, forceRefresh);
if (refresh) {
setMembers(response.list);
@@ -121,14 +126,14 @@ const GroupMembersScreen: React.FC = () => {
// 初始加载
useEffect(() => {
loadMembers(1, true);
loadMembers(1, true, true);
}, [groupId]);
// 下拉刷新
const onRefresh = useCallback(() => {
setRefreshing(true);
setHasMore(true);
loadMembers(1, true);
loadMembers(1, true, true);
}, [loadMembers]);
// 加载更多
@@ -246,6 +251,8 @@ const GroupMembersScreen: React.FC = () => {
}
return m;
}));
// 强制刷新远端状态,避免命中旧缓存导致解禁后仍显示禁言
await loadMembers(1, true, true);
setActionModalVisible(false);
Alert.alert('成功', `${actionText}`);

View File

@@ -72,9 +72,10 @@ const truncateDisplayName = (name: string, maxLength: number = MAX_CONVERSATION_
*/
const AsyncMessagePreview: React.FC<{
segments?: MessageSegment[];
status?: string;
isGroupChat?: boolean;
senderName?: string;
}> = ({ segments, isGroupChat, senderName }) => {
}> = ({ segments, status, isGroupChat, senderName }) => {
const [displayText, setDisplayText] = useState<string>('');
const isMountedRef = useRef(true);
@@ -82,6 +83,13 @@ const AsyncMessagePreview: React.FC<{
isMountedRef.current = true;
const loadPreview = async () => {
if (status === 'recalled') {
if (isMountedRef.current) {
setDisplayText('消息已撤回');
}
return;
}
const initialText = extractTextFromSegments(segments);
if (isMountedRef.current) {
setDisplayText(initialText);
@@ -112,7 +120,7 @@ const AsyncMessagePreview: React.FC<{
return () => {
isMountedRef.current = false;
};
}, [segments]);
}, [segments, status]);
if (!displayText) return null;
@@ -556,6 +564,7 @@ export const MessageListScreen: React.FC = () => {
) : (
<AsyncMessagePreview
segments={item.last_message?.segments}
status={item.last_message?.status}
isGroupChat={isGroupChat}
senderName={getSenderName()}
/>

View File

@@ -27,9 +27,6 @@ import { uploadService } from '../../../../services/uploadService';
import { ApiError } from '../../../../services/api';
// 【新架构】使用 MessageManager
import { useChat, useGroupTyping, useGroupMuted, messageManager } from '../../../../stores';
import {
websocketService,
} from '../../../../services/websocketService';
import { groupService } from '../../../../services/groupService';
import { userManager } from '../../../../stores/userManager';
import { groupManager } from '../../../../stores/groupManager';
@@ -728,14 +725,7 @@ export const useChatScreen = () => {
const segments = buildTextSegments(trimmedText, replyingTo);
if (isGroupChat && routeGroupId) {
// 群聊消息发送
websocketService.sendGroupChatMessage({
conversationId: conversationId,
groupId: routeGroupId,
segments: segments,
mentionUsers: selectedMentions.length > 0 ? selectedMentions : undefined,
mentionAll: mentionAll || undefined,
});
await messageService.sendMessageByAction('group', conversationId, segments);
setInputText('');
setSelectedMentions([]);
@@ -790,12 +780,7 @@ export const useChatScreen = () => {
const segments = buildImageSegments(uploadResult.url, uploadResult.url, undefined, undefined, replyingTo);
if (isGroupChat && routeGroupId) {
websocketService.sendGroupChatMessage({
conversationId: conversationId,
groupId: routeGroupId,
mediaUrl: uploadResult.url,
segments: segments,
});
await messageService.sendMessageByAction('group', conversationId, segments);
setReplyingTo(null);
} else {
// 【新架构】私聊图片通过 MessageManager 发送
@@ -922,12 +907,7 @@ export const useChatScreen = () => {
const segments = buildImageSegments(stickerUrl, stickerUrl, undefined, undefined, replyingTo);
if (isGroupChat && routeGroupId) {
websocketService.sendGroupChatMessage({
conversationId: conversationId,
groupId: routeGroupId,
mediaUrl: stickerUrl,
segments: segments,
});
await messageService.sendMessageByAction('group', conversationId, segments);
setReplyingTo(null);
} else {
// 【新架构】私聊表情通过 MessageManager 发送
@@ -983,7 +963,7 @@ export const useChatScreen = () => {
const handleRecall = useCallback(async (messageId: string) => {
try {
if (isGroupChat && routeGroupId) {
websocketService.sendGroupRecall(routeGroupId, conversationId!, messageId);
await messageService.recallMessage(messageId);
} else {
await messageService.recallMessage(messageId);
}

View File

@@ -145,7 +145,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
<Text style={styles.senderName}>{item.sender?.nickname || item.sender?.username}</Text>
)}
<Text style={[styles.messageText, isMe ? styles.messageTextMe : styles.messageTextOther]}>
{extractTextFromSegments(item.segments)}
{item.status === 'recalled' ? '消息已撤回' : extractTextFromSegments(item.segments)}
</Text>
</View>
{isMe && (