refactor: streamline post sync, fix image gallery, and clean up chat screen

- **Post sync optimization**: Clear store immediately when params change to prevent flashing old posts; replace instead of merge on refresh
- **ImageGallery fix**: Use idempotent download option, migrate to Asset.create() API, fix Android file path requirement, ensure proper cleanup
- **UserProfileScreen**: Add ImageGallery for post image viewing with consistent implementation
- **SearchScreen**: Add entrance animation and empty state
- **ChatScreen**: Remove unused state variables (lastSeq, firstSeq, isProgrammaticScrollRef) and dead imports
This commit is contained in:
lafay
2026-06-18 02:29:54 +08:00
parent 96e8de18bf
commit a921aacefd
11 changed files with 248 additions and 190 deletions

View File

@@ -159,7 +159,6 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
longPressMenuVisible,
selectedMessage,
selectedMessageId,
setSelectedMessageId,
menuPosition,
isGroupChat,
groupInfo,
@@ -207,7 +206,6 @@ export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
handleMentionAll,
getSenderInfo,
getTypingHint,
getInputBottom,
handleDismiss,
navigateToInfo,
navigateToChatSettings,

View File

@@ -2,7 +2,7 @@
* ChatScreen 类型定义
*/
import { MessageResponse, UserDTO, GroupMemberResponse, GroupResponse } from '../../../../types/dto';
import { MessageResponse, UserDTO, GroupMemberResponse } from '../../../../types/dto';
// 面板类型
export type PanelType = 'none' | 'emoji' | 'more' | 'mention';
@@ -215,39 +215,3 @@ export interface SwipeableMessageBubbleProps {
onReply: () => void;
enabled?: boolean;
}
// ChatScreen 状态接口
export interface ChatScreenState {
// 基础状态
messages: GroupMessage[];
conversationId: string | null;
inputText: string;
otherUser: UserDTO | null;
currentUser: UserDTO | null;
keyboardHeight: number;
loading: boolean;
sending: boolean;
currentUserId: string;
lastSeq: number;
otherUserLastReadSeq: number;
activePanel: PanelType;
sendingImage: boolean;
// 回复消息状态
replyingTo: GroupMessage | null;
// 长按菜单状态
longPressMenuVisible: boolean;
selectedMessage: GroupMessage | null;
// 群聊相关状态
groupInfo: GroupResponse | null;
groupMembers: GroupMemberResponse[];
typingUsers: string[];
currentUserRole: UserRole;
mentionQuery: string;
selectedMentions: string[];
mentionAll: boolean;
isMuted: boolean;
muteAll: boolean;
}

View File

@@ -183,8 +183,6 @@ export const useChatScreen = (props?: ChatScreenProps) => {
const [keyboardHeight, setKeyboardHeight] = useState(0);
const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false);
const [lastSeq, setLastSeq] = useState<number>(0);
const [firstSeq, setFirstSeq] = useState<number>(0);
const [otherUserLastReadSeq, setOtherUserLastReadSeq] = useState<number>(0);
const [activePanel, setActivePanel] = useState<PanelType>('none');
const [sendingImage, setSendingImage] = useState(false);
@@ -198,11 +196,9 @@ export const useChatScreen = (props?: ChatScreenProps) => {
// 滚动状态机 refsTelegram/Element 风格:底部粘附 + 阅读锚点)
const scrollPositionRef = useRef({ contentHeight: 0, scrollY: 0, viewportHeight: 0 });
const hasInitialAnchorDoneRef = useRef(false);
const prevMessageCountRef = useRef(0);
const prevLatestSeqRef = useRef(0);
const prevMarkedReadSeqRef = useRef(0);
const enterMarkedKeyRef = useRef<string>('');
const isProgrammaticScrollRef = useRef(false);
const suppressAutoFollowRef = useRef(false);
const isBrowsingHistoryRef = useRef(false);
const hasShownMessageListRef = useRef(false);
@@ -231,6 +227,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
setGroupInfo(prev => prev ? prev : { name: name || undefined, avatar: avatar || null });
}
}, [isGroupChat, effectiveGroupName, effectiveGroupAvatar]);
const [currentUserRole, setCurrentUserRole] = useState<UserRole>('member');
const [mentionQuery, setMentionQuery] = useState<string>('');
const [selectedMentions, setSelectedMentions] = useState<string[]>([]);
@@ -371,10 +368,9 @@ export const useChatScreen = (props?: ChatScreenProps) => {
};
}, [isGroupChat, otherUserId]);
// 进入新会话时重置滚动状态
// 进入新会话时重置滚动/草稿状态
useEffect(() => {
hasInitialAnchorDoneRef.current = false;
prevMessageCountRef.current = 0;
prevLatestSeqRef.current = 0;
prevMarkedReadSeqRef.current = 0;
enterMarkedKeyRef.current = '';
@@ -383,9 +379,6 @@ export const useChatScreen = (props?: ChatScreenProps) => {
hasShownMessageListRef.current = false;
historyLoadingLockUntilRef.current = 0;
scrollToSeqRef.current = routeScrollToSeq ?? null;
}, [conversationId]);
useEffect(() => {
setPendingAttachments([]);
}, [conversationId]);
@@ -405,14 +398,10 @@ export const useChatScreen = (props?: ChatScreenProps) => {
const scrollToLatest = useCallback((animated: boolean, force: boolean = false, reason: string = 'unknown') => {
if (!force && (suppressAutoFollowRef.current || isBrowsingHistoryRef.current || isHistoryLoadingLocked())) return;
// inverted 列表下,最新消息端对应 offset=0
isProgrammaticScrollRef.current = true;
flatListRef.current?.scrollToOffset({
offset: 0,
animated,
});
setTimeout(() => {
isProgrammaticScrollRef.current = false;
}, animated ? 220 : 32);
}, [flatListRef, isHistoryLoadingLocked]);
const isNearBottom = useCallback(() => {
@@ -422,18 +411,23 @@ export const useChatScreen = (props?: ChatScreenProps) => {
}, [scrollPositionRef]);
// 首屏加载完成后仅锚定一次到最新消息端(有 scrollToSeq 时跳过)
// 注意:必须在消息首次出现时立刻把 hasInitialAnchorDoneRef 置为 true
// 不能再依赖 viewportHeight 等通过 ref 同步的值——否则后续首次 loadMoreHistory
// 让 messages.length 变化时此 effect 会被再度命中,并以 force=true 调用
// scrollToLatest 强行把列表拉回最底端,造成"上滑首次加载更多就回底"的问题。
useEffect(() => {
if (
loading ||
loadingMore ||
messages.length === 0 ||
hasInitialAnchorDoneRef.current ||
scrollPositionRef.current.viewportHeight <= 0
hasInitialAnchorDoneRef.current
) {
return;
}
hasInitialAnchorDoneRef.current = true;
if (scrollToSeqRef.current != null) return; // 由下方 scrollToSeq effect 处理
// inverted FlashList 天然从 offset=0最新端开始
// 这里仅做一次保险锚定,即使布局尚未就绪也不会有副作用。
const timer = setTimeout(() => {
scrollToLatest(false, true, 'initial-anchor');
}, 0);
@@ -444,10 +438,8 @@ export const useChatScreen = (props?: ChatScreenProps) => {
// 仅当"最新端消息 seq 增长"且用户在底部附近时才跟随;
// 历史加载只会增加旧消息,不会提升 latest seq因此不会触发回底。
useEffect(() => {
const currentCount = messages.length;
const latestSeq = currentCount > 0 ? Math.max(...messages.map(m => m.seq || 0)) : 0;
const latestSeq = messages.length > 0 ? Math.max(...messages.map(m => m.seq || 0)) : 0;
const prevLatestSeq = prevLatestSeqRef.current;
prevMessageCountRef.current = currentCount;
prevLatestSeqRef.current = latestSeq;
if (loading || loadingMore) return;
@@ -577,33 +569,20 @@ export const useChatScreen = (props?: ChatScreenProps) => {
};
}, [routeUserId, conversationId, currentUserId, isGroupChat]);
// 加载更多历史消息inverted 下保持阅读锚点
// 加载更多历史消息inverted + maintainVisibleContentPosition 由列表原生保持位置
const loadMoreHistory = useCallback(async () => {
if (!conversationId || !hasMoreHistory || loadingMore) {
return;
}
// 历史加载期间禁止新消息自动跟随到底
// 历史加载期间禁止"新消息自动跟随到底"
suppressAutoFollowRef.current = true;
isBrowsingHistoryRef.current = true;
historyLoadingLockUntilRef.current = Date.now() + 3000;
// 保存加载前的滚动位置和内容高度
const scrollYBefore = scrollPositionRef.current.scrollY;
const contentHeightBefore = scrollPositionRef.current.contentHeight;
setLoadingMore(true);
try {
await loadMoreMessages();
// 更新 firstSeq
if (messages.length > 0) {
const minSeq = Math.min(...messages.map(m => m.seq));
setFirstSeq(minSeq);
}
// inverted + maintainVisibleContentPosition 下由列表原生保持位置
// 不做手动 scrollToOffset避免与原生锚点冲突导致回到底部
} catch (error) {
console.error('加载历史消息失败:', error);
} finally {
@@ -611,7 +590,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
// 加载结束后继续保留短暂锁窗,避免布局结算阶段误触自动回底
historyLoadingLockUntilRef.current = Date.now() + 800;
}
}, [conversationId, hasMoreHistory, loadingMore, loadMoreMessages, messages]);
}, [conversationId, hasMoreHistory, loadingMore, loadMoreMessages]);
// 从搜索结果跳转:滚动到目标 seq
useEffect(() => {
@@ -660,13 +639,9 @@ export const useChatScreen = (props?: ChatScreenProps) => {
isBrowsingHistoryRef.current = false;
// FlashList 在 inverted 模式下 scrollToOffset({ offset: 0 }) 可能无法真正滚到最底部,
// 因为列表内容高度变化后最小 offset 可能不是 0。先尝试滚到负值确保到底再补偿回 0。
isProgrammaticScrollRef.current = true;
flatListRef.current?.scrollToOffset({ offset: -99999, animated: false });
requestAnimationFrame(() => {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
setTimeout(() => {
isProgrammaticScrollRef.current = false;
}, 220);
});
}, [flatListRef]);
@@ -700,6 +675,9 @@ export const useChatScreen = (props?: ChatScreenProps) => {
// 自动标记已读QQ/Telegram 风格):
// 仅当出现更大的 latest seq且用户在最新端附近时才上报。
// 历史加载/浏览历史不会触发 read。
// 注意:不能复用 prevLatestSeqRef 做判定——它被新消息跟随 effect 无条件推进,
// 会导致这里的去重判断永远成立,标记已读逻辑事实上不会执行。
// 这里只依赖专属的 prevMarkedReadSeqRef 做去重。
useEffect(() => {
if (!conversationId || messages.length === 0) return;
if (loading || loadingMore) return;
@@ -709,10 +687,6 @@ export const useChatScreen = (props?: ChatScreenProps) => {
const latestSeq = Math.max(...messages.map(m => m.seq), 0);
if (latestSeq <= 0) return;
// 没有新增最新消息,不重复上报
if (latestSeq <= prevLatestSeqRef.current) return;
prevLatestSeqRef.current = latestSeq;
// 避免对同一 seq 重复标记
if (latestSeq <= prevMarkedReadSeqRef.current) return;
prevMarkedReadSeqRef.current = latestSeq;
@@ -722,18 +696,6 @@ export const useChatScreen = (props?: ChatScreenProps) => {
});
}, [messages, conversationId, markAsRead, loading, loadingMore, isNearBottom]);
// 使用 ref 存储 groupMembers
const groupMembersRef = useRef(groupMembers);
useEffect(() => {
groupMembersRef.current = groupMembers;
}, [groupMembers]);
// 使用 ref 存储 currentUserId
const currentUserIdRef = useRef(currentUserId);
useEffect(() => {
currentUserIdRef.current = currentUserId;
}, [currentUserId]);
// 监听键盘事件
useEffect(() => {
const keyboardWillShow = (e: KeyboardEvent) => {
@@ -1476,20 +1438,15 @@ export const useChatScreen = (props?: ChatScreenProps) => {
setActivePanel('none');
}, []);
// 撤回消息 - 现在通过 MessageManager 处理
// 撤回消息 - 现在通过 MessageManager 处理(撤回事件由 MessageManager 同步状态)
const handleRecall = useCallback(async (messageId: string) => {
try {
if (isGroupChat && effectiveGroupId) {
await messageService.recallMessage(messageId);
} else {
await messageService.recallMessage(messageId);
}
// 不需要手动更新状态MessageManager 会处理撤回事件
await messageService.recallMessage(messageId);
} catch (error) {
console.error('撤回消息失败:', error);
Alert.alert('撤回失败', '无法撤回消息');
}
}, [isGroupChat, effectiveGroupId, conversationId]);
}, []);
// 长按消息显示操作菜单
const handleLongPressMessage = useCallback((message: GroupMessage, position?: MenuPosition) => {
@@ -1528,8 +1485,6 @@ export const useChatScreen = (props?: ChatScreenProps) => {
if (!conversationId) return;
try {
setLastSeq(0);
setFirstSeq(0);
setHasMoreHistory(true);
await messageRepository.clearConversation(conversationId);
// 刷新消息列表
@@ -1538,9 +1493,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
console.error('清空会话失败:', error);
Alert.alert('清空失败', '无法清空聊天记录');
}
}, [conversationId, refreshMessages]);
// 回复消息
}, [conversationId, refreshMessages]); // 回复消息
const handleReplyMessage = useCallback((message: GroupMessage) => {
setReplyingTo(message);
textInputRef.current?.focus();
@@ -1593,26 +1546,6 @@ export const useChatScreen = (props?: ChatScreenProps) => {
}
}, [groupTypingUsers, groupMembers]);
// 计算底部面板高度
const getPanelHeight = useCallback(() => {
if (activePanel === 'none') return 0;
if (activePanel === 'mention') return 250;
return 350;
}, [activePanel]);
// 计算输入框的bottom值
const getInputBottom = useCallback(() => {
if (keyboardHeight > 0) return keyboardHeight;
if (activePanel !== 'none') return getPanelHeight();
return 0;
}, [keyboardHeight, activePanel, getPanelHeight]);
// 计算消息列表的底部padding
const getListPaddingBottom = useCallback(() => {
if (activePanel !== 'none' && keyboardHeight === 0) return getPanelHeight();
return 0;
}, [keyboardHeight, activePanel, getPanelHeight]);
// 关闭所有面板
const handleDismiss = useCallback(() => {
Keyboard.dismiss();
@@ -1656,7 +1589,6 @@ export const useChatScreen = (props?: ChatScreenProps) => {
currentUserId,
keyboardHeight,
loading,
sending,
activePanel,
sendingImage,
uploadingAttachments,
@@ -1666,16 +1598,12 @@ export const useChatScreen = (props?: ChatScreenProps) => {
longPressMenuVisible,
selectedMessage,
selectedMessageId,
setSelectedMessageId,
menuPosition,
isGroupChat,
groupInfo,
groupMembers,
// 【改造】使用 MessageManager 的输入状态
typingUsers: groupTypingUsers,
currentUserRole,
mentionQuery,
selectedMentions,
isMuted,
muteAll,
followRestrictionHint,
@@ -1697,9 +1625,7 @@ export const useChatScreen = (props?: ChatScreenProps) => {
shouldShowTime,
handleInputChange,
handleSend,
handlePickImage,
removePendingAttachment,
handleTakePhoto,
handleMoreAction,
handleInsertEmoji,
handleSendSticker,
@@ -1718,9 +1644,6 @@ export const useChatScreen = (props?: ChatScreenProps) => {
handleMentionAll,
getSenderInfo,
getTypingHint,
getPanelHeight,
getInputBottom,
getListPaddingBottom,
handleDismiss,
navigateToInfo,
navigateToChatSettings,