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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
// 滚动状态机 refs(Telegram/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,
|
||||
|
||||
Reference in New Issue
Block a user