feat(messaging): implement three-tier reply message lazy loading with offline fallback
Add lazy loading pipeline for reply message previews that checks memory → SQLite → server in order, enabling offline hit and graceful degradation for deleted/inaccessible messages. Includes `getReplyMessage` API endpoint and `jumpToMessageSeq` helper for navigation to referenced messages.
feat(create): extract MomentComposer and LongPostComposer for dual posting modes
Refactor CreatePostScreen shell to delegate content editing to specialized composers based on postMode ('moment' or 'long'), enabling long posts with voting support. Add expandable FAB with mode selection on HomeScreen.
fix(navigation): use navigate instead of push to prevent duplicate chat instances
Replace router.push with router.navigate in notification bootstrap to avoid creating duplicate chat instances when already on the chat screen.
fix(ui): update author badge styling with proper text contrast
Change author badge background to use hex transparency and add dedicated text color for better readability on both light and dark themes.
chore: normalize filename handling in file uploads and improve file segment rendering
Use asset.name as authoritative filename, remove redundant shadows from file cards inside bubbles.
This commit is contained in:
@@ -46,6 +46,7 @@ import {
|
||||
} from './types';
|
||||
import { TIME_SEPARATOR_INTERVAL, MEMBERS_PAGE_SIZE, MAX_PENDING_CHAT_IMAGES } from './constants';
|
||||
import { messageRepository } from '@/database';
|
||||
import { useReplyMessage } from './useReplyMessage';
|
||||
|
||||
interface MentionRange {
|
||||
start: number;
|
||||
@@ -607,11 +608,16 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
const ascendingIndex = messages.findIndex(m => m.seq === targetSeq);
|
||||
const displayIndex = messages.length - 1 - ascendingIndex;
|
||||
setTimeout(() => {
|
||||
flatListRef.current?.scrollToIndex({
|
||||
index: displayIndex,
|
||||
animated: false,
|
||||
viewPosition: 0.5,
|
||||
});
|
||||
try {
|
||||
flatListRef.current?.scrollToIndex({
|
||||
index: displayIndex,
|
||||
animated: false,
|
||||
viewPosition: 0.5,
|
||||
});
|
||||
} catch {
|
||||
// FlashList 远距离跳转可能失败,降级到 offset 滚动
|
||||
flatListRef.current?.scrollToOffset?.({ offset: 0, animated: false });
|
||||
}
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
@@ -623,6 +629,45 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
}
|
||||
}, [loading, loadingMore, messages, hasMoreHistory, loadMoreHistory]);
|
||||
|
||||
/**
|
||||
* 跳转到指定 seq 的消息(供引用消息点击跳转复用 scrollToSeq 范式)。
|
||||
* 设置目标 seq 后由上方 effect 接管:内存命中则 scrollToIndex,
|
||||
* 未命中则循环 loadMoreHistory 直到目标进入视窗或历史耗尽。
|
||||
* @returns 是否已受理跳转(false 表示无更多历史且不在内存,无法定位)
|
||||
*/
|
||||
const jumpToMessageSeq = useCallback((seq: number): boolean => {
|
||||
if (!hasInitialAnchorDoneRef.current) return false;
|
||||
if (!Number.isFinite(seq) || seq <= 0) return false;
|
||||
|
||||
scrollToSeqRef.current = seq;
|
||||
isBrowsingHistoryRef.current = true;
|
||||
suppressAutoFollowRef.current = true;
|
||||
|
||||
// 目标已在内存:直接定位(effect 依赖未变不会自动重跑,这里同步处理命中情况)
|
||||
const ascendingIndex = messages.findIndex(m => m.seq === seq);
|
||||
if (ascendingIndex >= 0) {
|
||||
scrollToSeqRef.current = null;
|
||||
const displayIndex = messages.length - 1 - ascendingIndex;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
flatListRef.current?.scrollToIndex({ index: displayIndex, animated: true, viewPosition: 0.5 });
|
||||
} catch {
|
||||
flatListRef.current?.scrollToOffset?.({ offset: 0, animated: true });
|
||||
}
|
||||
}, 50);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 目标不在内存:kickstart 历史加载循环,由 scrollToSeq effect 接管后续
|
||||
if (hasMoreHistory) {
|
||||
loadMoreHistory();
|
||||
return true;
|
||||
}
|
||||
// 无更多历史且不在内存,无法定位
|
||||
scrollToSeqRef.current = null;
|
||||
return false;
|
||||
}, [messages, hasMoreHistory, loadMoreHistory, flatListRef]);
|
||||
|
||||
// 列表内容尺寸变化:仅同步内容高度,锚点补偿由 loadMoreHistory 统一处理
|
||||
const handleMessageListContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
|
||||
scrollPositionRef.current.contentHeight = contentHeight;
|
||||
@@ -886,6 +931,9 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
return map;
|
||||
}, [messages]);
|
||||
|
||||
// 引用消息回填:被引用消息不在已加载内存区间时,懒加载回填预览/跳转所需数据
|
||||
const { getCached: getCachedReply, ensureReplyMessage } = useReplyMessage();
|
||||
|
||||
// 选择@用户
|
||||
const handleSelectMention = useCallback((member: { user_id: string; nickname?: string; user?: { nickname?: string } }) => {
|
||||
const currentText = inputTextRef.current;
|
||||
@@ -1320,7 +1368,9 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
|
||||
const segments = buildFileSegments(
|
||||
uploaded.url,
|
||||
uploaded.name || asset.name,
|
||||
// asset.name 是 DocumentPicker 返回的用户原始文件名(真实名),
|
||||
// 优先使用它;uploaded.name 为后端回传的展示名(已与 asset.name 一致)。
|
||||
asset.name || uploaded.name || 'file',
|
||||
uploaded.size ?? asset.size,
|
||||
uploaded.mime_type ?? asset.mimeType,
|
||||
replyingTo,
|
||||
@@ -1643,6 +1693,10 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
||||
effectiveGroupName,
|
||||
otherUserLastReadSeq,
|
||||
messageMap,
|
||||
// 引用消息回填(会话级缓存:内存 → 本地 SQLite → 服务端)
|
||||
getCachedReply,
|
||||
ensureReplyMessage,
|
||||
jumpToMessageSeq,
|
||||
loadingMore,
|
||||
hasMoreHistory,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user