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

@@ -437,7 +437,6 @@ export const HomeScreen: React.FC = () => {
refreshing: isRefreshing,
hasMore,
error,
reset,
} = useDifferentialPosts<Post>(
[],
{
@@ -497,8 +496,9 @@ export const HomeScreen: React.FC = () => {
useEffect(() => {
let cancelled = false;
const run = async () => {
reset();
await new Promise(resolve => requestAnimationFrame(resolve));
// 注意:不再在此处调用 reset()。
// fetchPosts 内部会在参数变化时立即清空 store 中的旧数据并设置 loading
// 这样可以避免本地清空后、请求发起前的间隙渲染到上一个分区的帖子造成闪烁。
if (!cancelled) {
await refresh();
}

View File

@@ -12,6 +12,7 @@ import {
TouchableOpacity,
ScrollView,
RefreshControl,
Animated,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
@@ -23,7 +24,7 @@ import { useUserStore } from '../../stores';
import { postService, authService } from '../../services';
import { tradeService } from '../../services/trade/tradeService';
import { postSyncService } from '@/services/post';
import { PostCard, TabBar, SearchBar } from '../../components/business';
import { PostCard, SearchBar } from '../../components/business';
import { TradeCard } from '../../components/business/TradeCard/TradeCard';
import type { PostCardAction } from '../../components/business/PostCard';
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
@@ -94,6 +95,25 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, homeTab = 's
// 保存当前搜索关键词用于Tab切换时重新搜索
const [currentKeyword, setCurrentKeyword] = useState('');
// 入场动画
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(20)).current;
useEffect(() => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 250,
useNativeDriver: true,
}),
]).start();
}, []);
const searchExtraParams = useMemo(() => ({ query: currentKeyword }), [currentKeyword]);
const {
@@ -508,10 +528,16 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, homeTab = 's
if (hasSearched) return null;
return (
<View style={[styles.suggestionsContainer, { paddingHorizontal: responsivePadding }]}>
<Animated.View
style={[
styles.suggestionsContainer,
{ paddingHorizontal: responsivePadding },
{ opacity: fadeAnim, transform: [{ translateY: slideAnim }] }
]}
>
{/* 搜索历史 */}
{history.length > 0 && (
<View style={[styles.section, { marginTop: responsiveGap }]}>
<View style={[styles.section, { marginTop: responsiveGap * 1.5 }]}>
<View style={styles.sectionHeader}>
<Text
variant="body"
@@ -522,7 +548,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, homeTab = 's
>
</Text>
<TouchableOpacity onPress={handleClearHistory}>
<TouchableOpacity onPress={handleClearHistory} activeOpacity={0.7}>
<MaterialCommunityIcons name="delete-outline" size={isDesktop ? 22 : 18} color={colors.text.secondary} />
</TouchableOpacity>
</View>
@@ -540,6 +566,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, homeTab = 's
}
]}
onPress={() => handleHistoryPress(keyword)}
activeOpacity={0.7}
>
<MaterialCommunityIcons name="history" size={isDesktop ? 16 : 14} color={colors.text.secondary} />
<Text
@@ -557,7 +584,18 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, homeTab = 's
</View>
</View>
)}
</View>
{/* 空状态提示 */}
{history.length === 0 && (
<View style={styles.emptySuggestions}>
<EmptyState
title="开始搜索"
description="输入关键词搜索帖子、用户或商品"
icon="magnify"
/>
</View>
)}
</Animated.View>
);
};
@@ -581,6 +619,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, homeTab = 's
onSubmit={handleSearch}
placeholder={isMarket ? "搜索商品、用户" : "搜索帖子、用户"}
autoFocus
compact
/>
</View>
<TouchableOpacity
@@ -598,20 +637,30 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, homeTab = 's
</TouchableOpacity>
</View>
{/* Tab切换 */}
{/* Tab切换 - 与主页风格一致的下划线样式 */}
<View style={styles.tabWrapper}>
<TabBar
tabs={TABS}
activeIndex={activeIndex}
onTabChange={(index) => {
setActiveIndex(index);
if (currentKeyword && hasSearched) {
performSearch(currentKeyword);
}
}}
variant="modern"
icons={isMarket ? ['shopping-outline', 'account-outline'] : ['file-document-outline', 'account-outline']}
/>
<View style={[styles.homeTabSwitcher, { paddingHorizontal: responsivePadding }]}>
{TABS.map((tab, index) => {
const isActive = activeIndex === index;
return (
<TouchableOpacity
key={tab}
activeOpacity={0.7}
style={[styles.homeTabItem, isActive && styles.homeTabItemActive]}
onPress={() => {
setActiveIndex(index);
if (currentKeyword && hasSearched) {
performSearch(currentKeyword);
}
}}
>
<Text style={isActive ? styles.homeTabTextActive : styles.homeTabText}>
{tab}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
{/* 内容区域 */}
@@ -629,15 +678,12 @@ function createSearchScreenStyles(colors: AppColors) {
searchHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: `${colors.divider}70`,
backgroundColor: colors.background.default,
},
searchShell: {
flex: 1,
},
cancelButton: {
backgroundColor: `${colors.primary.main}12`,
borderRadius: borderRadius.full,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
@@ -647,13 +693,43 @@ function createSearchScreenStyles(colors: AppColors) {
fontWeight: '600',
},
tabWrapper: {
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: `${colors.divider}50`,
backgroundColor: colors.background.default,
paddingVertical: spacing.xs,
},
homeTabSwitcher: {
flexDirection: 'row',
alignItems: 'center',
gap: 20,
},
homeTabItem: {
paddingVertical: spacing.sm,
alignItems: 'center',
justifyContent: 'center',
borderBottomWidth: 2,
borderBottomColor: 'transparent',
},
homeTabItemActive: {
borderBottomColor: colors.text.primary,
},
homeTabText: {
fontSize: 18,
fontWeight: '400',
color: colors.text.hint,
},
homeTabTextActive: {
fontSize: 18,
fontWeight: '700',
color: colors.text.primary,
},
suggestionsContainer: {
flex: 1,
},
emptySuggestions: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
marginTop: -40,
},
section: {
marginTop: spacing.md,
},
@@ -674,13 +750,14 @@ function createSearchScreenStyles(colors: AppColors) {
tag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: `${colors.primary.main}10`,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.full,
borderWidth: 0,
borderWidth: 1,
borderColor: colors.divider,
},
tagText: {
marginLeft: spacing.xs,
color: colors.primary.main,
color: colors.text.secondary,
fontWeight: '500',
},
userCard: {

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,

View File

@@ -16,7 +16,8 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useAppColors } from '../../theme';
import { Post } from '../../types';
import { PostCard, TabBar, UserProfileHeader } from '../../components/business';
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
import type { PostCardAction } from '../../components/business/PostCard';
import { Loading, EmptyState, ResponsiveContainer, ImageGallery, ImageGridItem } from '../../components/common';
import { useResponsive } from '../../hooks';
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles } from './useUserProfile';
@@ -116,6 +117,39 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
currentUser,
} = useUserProfile({ mode, userId, isDesktop, isTablet });
// 图片查看器状态(与 HomeScreen / PostDetailScreen 一致)
const [showImageViewer, setShowImageViewer] = useState(false);
const [postImages, setPostImages] = useState<ImageGridItem[]>([]);
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const closeImageViewer = useCallback(() => setShowImageViewer(false), []);
const stableGalleryImages = useMemo(
() =>
postImages.map((img, i) => ({
id: img.id || img.url || `img-${i}`,
url: img.url || img.uri || '',
})),
[postImages]
);
// 包装 handlePostAction在 hook 通用逻辑之上处理 imagePress
const onPostAction = useCallback(
(post: Post, action: PostCardAction) => {
if (action.type === 'imagePress') {
const images = action.payload?.images;
const imageIndex = action.payload?.imageIndex;
if (images && imageIndex !== undefined) {
setPostImages(images);
setSelectedImageIndex(imageIndex);
setShowImageViewer(true);
}
return;
}
handlePostAction(post, action);
},
[handlePostAction]
);
// 当前显示的帖子列表
const currentPosts = activeTab === 0 ? posts : favorites;
@@ -130,12 +164,12 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
]}>
<PostCard
post={item}
onAction={(action) => handlePostAction(item, action)}
onAction={(action) => onPostAction(item, action)}
isPostAuthor={isPostAuthor}
/>
</View>
);
}, [currentUser?.id, handlePostAction, currentPosts.length]);
}, [currentUser?.id, onPostAction, currentPosts.length]);
const postKeyExtractor = useCallback((item: Post) => item.id, []);
@@ -269,6 +303,15 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
</View>
</View>
</ResponsiveContainer>
{/* 图片查看器 */}
<ImageGallery
visible={showImageViewer}
images={stableGalleryImages}
initialIndex={selectedImageIndex}
onClose={closeImageViewer}
enableSave
/>
</SafeAreaView>
);
}
@@ -310,6 +353,15 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
}
drawDistance={250}
/>
{/* 图片查看器 */}
<ImageGallery
visible={showImageViewer}
images={stableGalleryImages}
initialIndex={selectedImageIndex}
onClose={closeImageViewer}
enableSave
/>
</SafeAreaView>
);
};