2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 首页 HomeScreen
|
|
|
|
|
|
* 胡萝卜BBS - 首页展示
|
|
|
|
|
|
* 支持列表和多列网格模式(响应式布局)
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-25 01:29:41 +08:00
|
|
|
|
import React, { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } from 'react';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
View,
|
|
|
|
|
|
FlatList,
|
|
|
|
|
|
ScrollView,
|
2026-03-24 22:27:33 +08:00
|
|
|
|
Platform,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
StyleSheet,
|
|
|
|
|
|
RefreshControl,
|
|
|
|
|
|
StatusBar,
|
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
|
NativeSyntheticEvent,
|
2026-03-25 01:29:41 +08:00
|
|
|
|
NativeScrollEvent,
|
2026-03-10 12:58:23 +08:00
|
|
|
|
Alert,
|
|
|
|
|
|
Clipboard,
|
2026-03-18 00:25:46 +08:00
|
|
|
|
Modal,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} from 'react-native';
|
|
|
|
|
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
2026-03-25 01:30:00 +08:00
|
|
|
|
import { useRouter, useFocusEffect } from 'expo-router';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
|
|
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
2026-03-25 05:16:54 +08:00
|
|
|
|
import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { Post } from '../../types';
|
2026-03-26 03:41:43 +08:00
|
|
|
|
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { useCurrentUser } from '../../stores/authStore';
|
2026-03-24 22:27:33 +08:00
|
|
|
|
import { channelService, postService } from '../../services';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
2026-03-24 03:02:54 +08:00
|
|
|
|
import type { PostCardAction } from '../../components/business/PostCard';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
|
|
|
|
|
|
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
import { useDifferentialPosts } from '../../hooks/useDifferentialPosts';
|
|
|
|
|
|
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
2026-03-16 17:47:10 +08:00
|
|
|
|
import { SearchScreen } from './SearchScreen';
|
2026-03-18 00:25:46 +08:00
|
|
|
|
import { CreatePostScreen } from '../create/CreatePostScreen';
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import * as hrefs from '../../navigation/hrefs';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-24 22:27:33 +08:00
|
|
|
|
const TABS = ['关注', '最新', '热门'];
|
|
|
|
|
|
const TAB_ICONS = ['account-heart-outline', 'clock-outline', 'fire'];
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const DEFAULT_PAGE_SIZE = 20;
|
|
|
|
|
|
const SWIPE_TRANSLATION_THRESHOLD = 40;
|
|
|
|
|
|
const SWIPE_COOLDOWN_MS = 300;
|
|
|
|
|
|
const MOBILE_TAB_BAR_HEIGHT = 64;
|
|
|
|
|
|
const MOBILE_TAB_FLOATING_MARGIN = 12;
|
|
|
|
|
|
const MOBILE_FAB_GAP = 12;
|
2026-03-25 01:30:00 +08:00
|
|
|
|
/** 首页纵向滑动超过此阈值视为明确向下/向上划,用于隐藏或显示底部 Tab */
|
|
|
|
|
|
const TAB_BAR_SCROLL_DELTA_Y = 10;
|
|
|
|
|
|
/** 停止滑动多久后自动恢复底部 Tab */
|
|
|
|
|
|
const TAB_BAR_IDLE_RESTORE_MS = 2200;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
type ViewMode = 'list' | 'grid';
|
2026-03-24 05:18:22 +08:00
|
|
|
|
type PostType = 'follow' | 'hot' | 'latest';
|
2026-03-24 22:27:33 +08:00
|
|
|
|
type LatestCapsule = { id: string; name: string };
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-25 05:16:54 +08:00
|
|
|
|
function createHomeStyles(colors: AppColors) {
|
|
|
|
|
|
return StyleSheet.create({
|
|
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
},
|
|
|
|
|
|
header: {
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
},
|
|
|
|
|
|
searchWrapper: {
|
|
|
|
|
|
paddingTop: spacing.lg,
|
|
|
|
|
|
paddingBottom: spacing.sm,
|
|
|
|
|
|
shadowColor: 'transparent',
|
|
|
|
|
|
shadowOffset: { width: 0, height: 0 },
|
|
|
|
|
|
shadowOpacity: 0,
|
|
|
|
|
|
shadowRadius: 0,
|
|
|
|
|
|
elevation: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
homeTabBar: {
|
|
|
|
|
|
marginTop: spacing.xs,
|
|
|
|
|
|
marginBottom: spacing.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
viewToggleBtn: {
|
|
|
|
|
|
width: 44,
|
|
|
|
|
|
height: 44,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
capsuleWrapper: {
|
|
|
|
|
|
paddingBottom: spacing.sm,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
},
|
|
|
|
|
|
capsuleList: {
|
|
|
|
|
|
gap: spacing.xs,
|
|
|
|
|
|
paddingRight: spacing.lg,
|
|
|
|
|
|
},
|
|
|
|
|
|
capsuleItem: {
|
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
|
paddingVertical: spacing.xs,
|
|
|
|
|
|
borderRadius: 999,
|
|
|
|
|
|
},
|
|
|
|
|
|
capsuleText: {
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
|
},
|
|
|
|
|
|
capsuleTextActive: {
|
|
|
|
|
|
color: colors.primary.main,
|
|
|
|
|
|
},
|
|
|
|
|
|
listContent: {
|
|
|
|
|
|
flexGrow: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
listHeader: {
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
},
|
|
|
|
|
|
contentContainer: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
listItem: {},
|
|
|
|
|
|
waterfallScroll: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
waterfallContainer: {
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
flexGrow: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
waterfallColumnsRow: {
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
|
},
|
|
|
|
|
|
waterfallColumn: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
waterfallItem: {},
|
|
|
|
|
|
floatingButton: {
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
right: 20,
|
|
|
|
|
|
bottom: 20,
|
|
|
|
|
|
width: 56,
|
|
|
|
|
|
height: 56,
|
|
|
|
|
|
borderRadius: 28,
|
|
|
|
|
|
backgroundColor: colors.primary.main,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
...shadows.lg,
|
|
|
|
|
|
},
|
|
|
|
|
|
floatingButtonDesktop: {
|
|
|
|
|
|
right: 40,
|
|
|
|
|
|
bottom: 40,
|
|
|
|
|
|
width: 64,
|
|
|
|
|
|
height: 64,
|
|
|
|
|
|
borderRadius: 32,
|
|
|
|
|
|
},
|
|
|
|
|
|
floatingButtonWide: {
|
|
|
|
|
|
right: 60,
|
|
|
|
|
|
bottom: 60,
|
|
|
|
|
|
width: 72,
|
|
|
|
|
|
height: 72,
|
|
|
|
|
|
borderRadius: 36,
|
|
|
|
|
|
},
|
|
|
|
|
|
loadingMoreFooter: {
|
|
|
|
|
|
paddingVertical: 20,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
export const HomeScreen: React.FC = () => {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const router = useRouter();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const insets = useSafeAreaInsets();
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
const { posts: storePosts } = useUserStore();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const currentUser = useCurrentUser();
|
2026-03-25 05:16:54 +08:00
|
|
|
|
const colors = useAppColors();
|
|
|
|
|
|
const resolvedScheme = useResolvedColorScheme();
|
|
|
|
|
|
const styles = useMemo(() => createHomeStyles(colors), [colors]);
|
|
|
|
|
|
const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content';
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 使用响应式 hook
|
|
|
|
|
|
const {
|
|
|
|
|
|
width,
|
|
|
|
|
|
isMobile,
|
|
|
|
|
|
isTablet,
|
|
|
|
|
|
isDesktop,
|
|
|
|
|
|
isWideScreen,
|
|
|
|
|
|
} = useResponsive();
|
|
|
|
|
|
|
|
|
|
|
|
// 响应式间距
|
|
|
|
|
|
const responsiveGap = useResponsiveSpacing({ xs: 4, sm: 6, md: 8, lg: 12, xl: 16 });
|
|
|
|
|
|
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
|
|
|
|
|
|
2026-03-24 22:27:33 +08:00
|
|
|
|
const [activeIndex, setActiveIndex] = useState(1);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
2026-03-24 22:27:33 +08:00
|
|
|
|
const [latestCapsules, setLatestCapsules] = useState<LatestCapsule[]>([{ id: '', name: '全部' }]);
|
|
|
|
|
|
const [activeCapsuleId, setActiveCapsuleId] = useState('');
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 图片查看器状态
|
|
|
|
|
|
const [showImageViewer, setShowImageViewer] = useState(false);
|
|
|
|
|
|
const [postImages, setPostImages] = useState<ImageGridItem[]>([]);
|
|
|
|
|
|
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
|
|
|
|
|
|
2026-03-16 17:47:10 +08:00
|
|
|
|
// 搜索显示状态(用于内嵌搜索页面)
|
|
|
|
|
|
const [showSearch, setShowSearch] = useState(false);
|
|
|
|
|
|
|
2026-03-18 00:25:46 +08:00
|
|
|
|
// 发帖弹窗状态
|
|
|
|
|
|
const [showCreatePost, setShowCreatePost] = useState(false);
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const postIdsRef = useRef<Set<string>>(new Set());
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const lastSwipeAtRef = useRef(0);
|
2026-03-23 03:58:26 +08:00
|
|
|
|
|
|
|
|
|
|
const isLoadingMoreRef = useRef(false);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-25 01:30:00 +08:00
|
|
|
|
const setBottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.setBottomTabBarHiddenByScroll);
|
|
|
|
|
|
const bottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.bottomTabBarHiddenByScroll);
|
2026-03-26 03:41:43 +08:00
|
|
|
|
const homeTabPressCount = useHomeTabPressStore((s) => s.pressCount);
|
2026-03-25 01:30:00 +08:00
|
|
|
|
const homeListScrollYRef = useRef(0);
|
|
|
|
|
|
const tabBarIdleRestoreTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
|
|
2026-03-26 03:41:43 +08:00
|
|
|
|
// FlatList 和 ScrollView 的 ref,用于滚动到顶部
|
|
|
|
|
|
const flatListRef = useRef<FlatList<Post> | null>(null);
|
|
|
|
|
|
const scrollViewRef = useRef<ScrollView | null>(null);
|
|
|
|
|
|
|
2026-03-25 01:30:00 +08:00
|
|
|
|
const clearTabBarIdleRestoreTimer = useCallback(() => {
|
|
|
|
|
|
if (tabBarIdleRestoreTimerRef.current != null) {
|
|
|
|
|
|
clearTimeout(tabBarIdleRestoreTimerRef.current);
|
|
|
|
|
|
tabBarIdleRestoreTimerRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const scheduleTabBarIdleRestore = useCallback(() => {
|
|
|
|
|
|
clearTabBarIdleRestoreTimer();
|
|
|
|
|
|
tabBarIdleRestoreTimerRef.current = setTimeout(() => {
|
|
|
|
|
|
setBottomTabBarHiddenByScroll(false);
|
|
|
|
|
|
tabBarIdleRestoreTimerRef.current = null;
|
|
|
|
|
|
}, TAB_BAR_IDLE_RESTORE_MS);
|
|
|
|
|
|
}, [clearTabBarIdleRestoreTimer, setBottomTabBarHiddenByScroll]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleHomeVerticalScroll = useCallback(
|
|
|
|
|
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
|
|
|
|
const y = event.nativeEvent.contentOffset.y;
|
|
|
|
|
|
const dy = y - homeListScrollYRef.current;
|
|
|
|
|
|
homeListScrollYRef.current = y;
|
|
|
|
|
|
|
|
|
|
|
|
scheduleTabBarIdleRestore();
|
|
|
|
|
|
|
|
|
|
|
|
if (y <= 0) {
|
|
|
|
|
|
setBottomTabBarHiddenByScroll(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (dy > TAB_BAR_SCROLL_DELTA_Y) {
|
|
|
|
|
|
setBottomTabBarHiddenByScroll(true);
|
|
|
|
|
|
} else if (dy < -TAB_BAR_SCROLL_DELTA_Y) {
|
|
|
|
|
|
setBottomTabBarHiddenByScroll(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[scheduleTabBarIdleRestore, setBottomTabBarHiddenByScroll]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
useFocusEffect(
|
|
|
|
|
|
useCallback(() => {
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
clearTabBarIdleRestoreTimer();
|
|
|
|
|
|
homeListScrollYRef.current = 0;
|
|
|
|
|
|
setBottomTabBarHiddenByScroll(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [clearTabBarIdleRestoreTimer, setBottomTabBarHiddenByScroll])
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (showSearch) {
|
|
|
|
|
|
clearTabBarIdleRestoreTimer();
|
|
|
|
|
|
setBottomTabBarHiddenByScroll(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [showSearch, clearTabBarIdleRestoreTimer, setBottomTabBarHiddenByScroll]);
|
|
|
|
|
|
|
2026-03-26 03:41:43 +08:00
|
|
|
|
// 监听首页 Tab 点击事件,滚动到顶部
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (homeTabPressCount > 0) {
|
|
|
|
|
|
// 滚动 FlatList 到顶部
|
|
|
|
|
|
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
|
|
|
|
|
// 滚动 ScrollView 到顶部(网格模式)
|
|
|
|
|
|
scrollViewRef.current?.scrollTo({ y: 0, animated: true });
|
|
|
|
|
|
// 重置底部 Tab 栏状态
|
|
|
|
|
|
setBottomTabBarHiddenByScroll(false);
|
|
|
|
|
|
homeListScrollYRef.current = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [homeTabPressCount, setBottomTabBarHiddenByScroll]);
|
|
|
|
|
|
|
2026-03-25 01:29:41 +08:00
|
|
|
|
/** 横向胶囊条滚动位置:切换频道刷新列表时不重置 */
|
|
|
|
|
|
const capsuleHScrollRef = useRef<ScrollView | null>(null);
|
|
|
|
|
|
const capsuleScrollXRef = useRef(0);
|
|
|
|
|
|
|
|
|
|
|
|
const restoreCapsuleStripScroll = useCallback(() => {
|
|
|
|
|
|
const x = capsuleScrollXRef.current;
|
|
|
|
|
|
if (x <= 0) return;
|
|
|
|
|
|
const scroll = () => capsuleHScrollRef.current?.scrollTo({ x, animated: false });
|
|
|
|
|
|
scroll();
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
requestAnimationFrame(scroll);
|
|
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const onCapsuleHorizontalScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
|
|
|
|
capsuleScrollXRef.current = e.nativeEvent.contentOffset.x;
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
// 构建一个以 postId 为 key 的 map,用于快速查找
|
|
|
|
|
|
const postsMap = useMemo(() => {
|
|
|
|
|
|
const map = new Map<string, Post>();
|
|
|
|
|
|
storePosts.forEach(post => {
|
|
|
|
|
|
map.set(post.id, post);
|
|
|
|
|
|
});
|
|
|
|
|
|
return map;
|
|
|
|
|
|
}, [storePosts]);
|
|
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
// 获取当前 tab 对应的帖子类型
|
|
|
|
|
|
const getPostType = useCallback((): PostType => {
|
|
|
|
|
|
switch (activeIndex) {
|
2026-03-24 22:27:33 +08:00
|
|
|
|
case 0: return 'follow';
|
|
|
|
|
|
case 1: return 'latest';
|
2026-03-20 23:00:27 +08:00
|
|
|
|
case 2: return 'hot';
|
2026-03-24 05:18:22 +08:00
|
|
|
|
default: return 'latest';
|
2026-03-20 23:00:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, [activeIndex]);
|
|
|
|
|
|
|
2026-03-24 22:27:33 +08:00
|
|
|
|
const isLatestTab = activeIndex === 1;
|
|
|
|
|
|
const currentChannelId = isLatestTab && activeCapsuleId ? activeCapsuleId : undefined;
|
|
|
|
|
|
|
2026-03-25 01:30:00 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
homeListScrollYRef.current = 0;
|
|
|
|
|
|
setBottomTabBarHiddenByScroll(false);
|
|
|
|
|
|
}, [activeIndex, currentChannelId, viewMode, setBottomTabBarHiddenByScroll]);
|
|
|
|
|
|
|
2026-03-25 01:29:41 +08:00
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
|
if (!isLatestTab) return;
|
|
|
|
|
|
restoreCapsuleStripScroll();
|
|
|
|
|
|
}, [isLatestTab, activeCapsuleId, latestCapsules.length, restoreCapsuleStripScroll]);
|
|
|
|
|
|
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
// 使用差异更新 Hook 获取帖子列表
|
2026-03-24 22:27:33 +08:00
|
|
|
|
const listKey = useMemo(
|
|
|
|
|
|
() => `home_${getPostType()}_${currentChannelId || 'all'}`,
|
|
|
|
|
|
[getPostType, currentChannelId]
|
|
|
|
|
|
);
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const {
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
posts,
|
|
|
|
|
|
loading: isLoading,
|
2026-03-24 01:19:09 +08:00
|
|
|
|
isInitialLoading,
|
|
|
|
|
|
isLoadingMore,
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
refreshing: isRefreshing,
|
2026-03-20 23:00:27 +08:00
|
|
|
|
hasMore,
|
|
|
|
|
|
error,
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
reset,
|
|
|
|
|
|
} = useDifferentialPosts<Post>(
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
listKey,
|
|
|
|
|
|
enableDiff: true,
|
|
|
|
|
|
enableBatching: true,
|
|
|
|
|
|
autoSubscribe: true,
|
|
|
|
|
|
}
|
2026-03-20 23:00:27 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
// 加载更多方法
|
|
|
|
|
|
const loadMore = useCallback(async () => {
|
2026-03-24 01:19:09 +08:00
|
|
|
|
if (hasMore && !isLoadingMore && !isLoadingMoreRef.current) {
|
2026-03-23 03:58:26 +08:00
|
|
|
|
isLoadingMoreRef.current = true;
|
2026-03-24 01:19:09 +08:00
|
|
|
|
const startedAt = Date.now();
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await processPostUseCase.loadMorePosts(listKey);
|
2026-03-24 01:19:09 +08:00
|
|
|
|
console.log('[PostPerf] home loadMore', {
|
|
|
|
|
|
listKey,
|
|
|
|
|
|
costMs: Date.now() - startedAt,
|
|
|
|
|
|
totalItems: posts.length,
|
|
|
|
|
|
});
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('加载更多失败:', err);
|
2026-03-23 03:58:26 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
isLoadingMoreRef.current = false;
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-24 01:19:09 +08:00
|
|
|
|
}, [posts.length, listKey, hasMore, isLoadingMore]);
|
2026-03-23 03:58:26 +08:00
|
|
|
|
|
2026-03-25 01:30:00 +08:00
|
|
|
|
// 网格模式滚动处理 - 检测是否滚动到底部 + 底部 Tab 显隐
|
|
|
|
|
|
const handleGridScroll = useCallback(
|
|
|
|
|
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
|
|
|
|
handleHomeVerticalScroll(event);
|
|
|
|
|
|
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
|
|
|
|
|
|
const scrollY = contentOffset.y;
|
|
|
|
|
|
const visibleHeight = layoutMeasurement.height;
|
|
|
|
|
|
const contentHeight = contentSize.height;
|
|
|
|
|
|
|
|
|
|
|
|
if (scrollY + visibleHeight >= contentHeight - 200) {
|
|
|
|
|
|
loadMore();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[handleHomeVerticalScroll, loadMore]
|
|
|
|
|
|
);
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 刷新方法 - 先获取正确类型的帖子,再刷新
|
|
|
|
|
|
const refresh = useCallback(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先获取正确类型的帖子
|
|
|
|
|
|
await processPostUseCase.fetchPosts(
|
2026-03-24 22:27:33 +08:00
|
|
|
|
{
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
pageSize: DEFAULT_PAGE_SIZE,
|
|
|
|
|
|
post_type: getPostType(),
|
|
|
|
|
|
channelId: currentChannelId,
|
|
|
|
|
|
},
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
listKey
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('刷新失败:', err);
|
|
|
|
|
|
}
|
2026-03-24 22:27:33 +08:00
|
|
|
|
}, [listKey, getPostType, currentChannelId]);
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
|
|
|
|
|
|
// Tab 切换时刷新数据并重置列表
|
2026-03-20 23:00:27 +08:00
|
|
|
|
useEffect(() => {
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
reset();
|
2026-03-20 23:00:27 +08:00
|
|
|
|
refresh();
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-03-24 22:27:33 +08:00
|
|
|
|
}, [activeIndex, currentChannelId]);
|
2026-03-20 23:00:27 +08:00
|
|
|
|
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
// 将获取到的原始帖子与 store 中的状态合并
|
|
|
|
|
|
// 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等)
|
|
|
|
|
|
const displayPosts = useMemo(() => {
|
|
|
|
|
|
return posts.map(post => {
|
|
|
|
|
|
const storePost = postsMap.get(post.id);
|
|
|
|
|
|
if (storePost) {
|
2026-03-25 01:30:00 +08:00
|
|
|
|
// store 同步点赞等状态;channel 等列表专用字段在 store 里常缺失,从列表帖合并
|
|
|
|
|
|
return {
|
|
|
|
|
|
...post,
|
|
|
|
|
|
...storePost,
|
|
|
|
|
|
channel: storePost.channel ?? post.channel,
|
|
|
|
|
|
};
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
return post;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [posts, postsMap]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-24 05:52:24 +08:00
|
|
|
|
/** 按 id 取当前列表中的最新 post,配合 PostCard memo 忽略 onAction 引用 */
|
|
|
|
|
|
const postByIdRef = useRef<Map<string, Post>>(new Map());
|
|
|
|
|
|
postByIdRef.current = new Map(displayPosts.map((p) => [p.id, p]));
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 根据屏幕尺寸确定网格列数
|
|
|
|
|
|
const gridColumns = useMemo(() => {
|
|
|
|
|
|
if (isWideScreen || width >= 1440) return 4;
|
|
|
|
|
|
if (isDesktop || width >= 1024) return 3;
|
|
|
|
|
|
if (isTablet || width >= 768) return 2;
|
|
|
|
|
|
return 2; // 移动端瀑布流保持2列
|
|
|
|
|
|
}, [width, isTablet, isDesktop, isWideScreen]);
|
|
|
|
|
|
|
|
|
|
|
|
// 列表模式下始终使用单列,宽屏下居中显示
|
|
|
|
|
|
const useMultiColumnList = useMemo(() => {
|
|
|
|
|
|
return false; // 改为始终返回false,使用单列布局
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 宽屏下内容最大宽度
|
|
|
|
|
|
const contentMaxWidth = useMemo(() => {
|
|
|
|
|
|
if (isWideScreen) return 800;
|
|
|
|
|
|
if (isDesktop) return 720;
|
|
|
|
|
|
if (isTablet) return 640;
|
|
|
|
|
|
return width; // 移动端使用全宽
|
|
|
|
|
|
}, [width, isTablet, isDesktop, isWideScreen]);
|
|
|
|
|
|
|
|
|
|
|
|
// 列表模式横向内边距:移动端适当收窄,减少两侧空白
|
|
|
|
|
|
const listHorizontalPadding = useMemo(() => {
|
|
|
|
|
|
if (isMobile) {
|
|
|
|
|
|
return Math.max(6, responsivePadding - responsiveGap);
|
|
|
|
|
|
}
|
|
|
|
|
|
return responsivePadding;
|
|
|
|
|
|
}, [isMobile, responsivePadding, responsiveGap]);
|
|
|
|
|
|
|
|
|
|
|
|
// 列表模式卡片宽度:宽屏限宽并居中,移动端占用更多可用宽度
|
|
|
|
|
|
const listItemWidth = useMemo(() => {
|
|
|
|
|
|
const availableWidth = Math.max(0, width - listHorizontalPadding * 2);
|
|
|
|
|
|
if (isDesktop || isWideScreen) {
|
|
|
|
|
|
return Math.min(contentMaxWidth, availableWidth);
|
|
|
|
|
|
}
|
|
|
|
|
|
return availableWidth;
|
|
|
|
|
|
}, [width, listHorizontalPadding, isDesktop, isWideScreen, contentMaxWidth]);
|
|
|
|
|
|
|
|
|
|
|
|
const floatingButtonBottom = useMemo(() => {
|
|
|
|
|
|
if (!isMobile) {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
2026-03-25 01:30:00 +08:00
|
|
|
|
if (bottomTabBarHiddenByScroll) {
|
|
|
|
|
|
return MOBILE_TAB_FLOATING_MARGIN + MOBILE_FAB_GAP + insets.bottom;
|
|
|
|
|
|
}
|
2026-03-21 03:22:28 +08:00
|
|
|
|
// TabBar 悬浮在底部,发帖按钮需要在 TabBar 上方
|
|
|
|
|
|
return MOBILE_TAB_BAR_HEIGHT + MOBILE_TAB_FLOATING_MARGIN * 2 + MOBILE_FAB_GAP + insets.bottom;
|
2026-03-25 01:30:00 +08:00
|
|
|
|
}, [isMobile, insets.bottom, bottomTabBarHiddenByScroll]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 切换视图模式
|
|
|
|
|
|
const toggleViewMode = () => {
|
|
|
|
|
|
setViewMode(prev => prev === 'list' ? 'grid' : 'list');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 切换Tab(手势/点击共用)
|
|
|
|
|
|
const changeTab = useCallback((nextIndex: number) => {
|
|
|
|
|
|
if (nextIndex < 0 || nextIndex >= TABS.length || nextIndex === activeIndex) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setActiveIndex(nextIndex);
|
|
|
|
|
|
}, [activeIndex]);
|
|
|
|
|
|
|
2026-03-24 22:27:33 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadChannels = async () => {
|
|
|
|
|
|
const list = await channelService.list();
|
|
|
|
|
|
const capsules: LatestCapsule[] = [
|
|
|
|
|
|
{ id: '', name: '全部' },
|
|
|
|
|
|
...list.map(item => ({ id: item.id, name: item.name })),
|
|
|
|
|
|
];
|
|
|
|
|
|
setLatestCapsules(capsules);
|
|
|
|
|
|
};
|
|
|
|
|
|
loadChannels();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const handleSwipeTabChange = useCallback((translationX: number) => {
|
|
|
|
|
|
setActiveIndex(prev => (
|
|
|
|
|
|
translationX < 0
|
|
|
|
|
|
? Math.min(prev + 1, TABS.length - 1)
|
|
|
|
|
|
: Math.max(prev - 1, 0)
|
|
|
|
|
|
));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const swipeGesture = useMemo(() => (
|
|
|
|
|
|
Gesture.Pan()
|
|
|
|
|
|
.runOnJS(true)
|
|
|
|
|
|
.activeOffsetX([-15, 15])
|
|
|
|
|
|
.failOffsetY([-20, 20])
|
2026-03-18 14:21:24 +08:00
|
|
|
|
.shouldCancelWhenOutside(true)
|
2026-03-09 21:29:03 +08:00
|
|
|
|
.onEnd((event) => {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
if (now - lastSwipeAtRef.current < SWIPE_COOLDOWN_MS) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Math.abs(event.translationX) < SWIPE_TRANSLATION_THRESHOLD) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastSwipeAtRef.current = now;
|
|
|
|
|
|
handleSwipeTabChange(event.translationX);
|
|
|
|
|
|
})
|
|
|
|
|
|
), [handleSwipeTabChange]);
|
|
|
|
|
|
|
2026-03-16 17:47:10 +08:00
|
|
|
|
// 跳转到搜索页(使用内嵌模式,不再依赖导航)
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const handleSearchPress = () => {
|
2026-03-16 17:47:10 +08:00
|
|
|
|
setShowSearch(true);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 跳转到帖子详情
|
|
|
|
|
|
const handlePostPress = (postId: string, scrollToComments: boolean = false) => {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefPostDetail(postId, scrollToComments));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 跳转到用户主页
|
|
|
|
|
|
const handleUserPress = (userId: string) => {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefUserProfile(userId));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 点赞帖子
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
const handleLike = async (post: Post) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (post.is_liked) {
|
|
|
|
|
|
await processPostUseCase.unlikePost(post.id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await processPostUseCase.likePost(post.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('点赞操作失败:', error);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 收藏帖子
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
const handleBookmark = async (post: Post) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (post.is_favorited) {
|
|
|
|
|
|
await processPostUseCase.unfavoritePost(post.id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await processPostUseCase.favoritePost(post.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('收藏操作失败:', error);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 分享帖子
|
2026-03-10 12:58:23 +08:00
|
|
|
|
const handleShare = async (post: Post) => {
|
|
|
|
|
|
if (!post?.id) return;
|
|
|
|
|
|
try {
|
2026-03-24 05:21:46 +08:00
|
|
|
|
const res = await postService.sharePost(post.id, { channel: 'copy_link' });
|
|
|
|
|
|
if (res.ok && res.shares_count != null) {
|
|
|
|
|
|
processPostUseCase.applyShareCountUpdate(post.id, res.shares_count);
|
|
|
|
|
|
}
|
2026-03-20 23:00:27 +08:00
|
|
|
|
} catch (shareError) {
|
|
|
|
|
|
console.error('上报分享次数失败:', shareError);
|
2026-03-10 12:58:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
const postUrl = `https://browser.littlelan.cn/posts/${encodeURIComponent(post.id)}`;
|
|
|
|
|
|
Clipboard.setString(postUrl);
|
|
|
|
|
|
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
2026-03-09 21:29:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
// 删除帖子 - 删除后刷新列表
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const handleDeletePost = async (postId: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const success = await postService.deletePost(postId);
|
|
|
|
|
|
if (success) {
|
2026-03-20 23:00:27 +08:00
|
|
|
|
// 刷新列表以移除已删除的帖子
|
|
|
|
|
|
refresh();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
console.error('删除帖子失败');
|
|
|
|
|
|
}
|
2026-03-20 23:00:27 +08:00
|
|
|
|
} catch (deleteError) {
|
|
|
|
|
|
console.error('删除帖子失败:', deleteError);
|
|
|
|
|
|
throw deleteError; // 重新抛出错误,让 PostCard 处理错误提示
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理图片点击 - 打开图片查看器
|
|
|
|
|
|
const handleImagePress = (images: ImageGridItem[], index: number) => {
|
|
|
|
|
|
setPostImages(images);
|
|
|
|
|
|
setSelectedImageIndex(index);
|
|
|
|
|
|
setShowImageViewer(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-24 03:02:54 +08:00
|
|
|
|
// 统一处理 PostCard 的所有操作
|
|
|
|
|
|
const handlePostAction = (post: Post, action: PostCardAction) => {
|
|
|
|
|
|
switch (action.type) {
|
|
|
|
|
|
case 'press':
|
|
|
|
|
|
handlePostPress(post.id);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'userPress':
|
|
|
|
|
|
const authorId = post.author?.id;
|
|
|
|
|
|
if (authorId) handleUserPress(authorId);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'like':
|
|
|
|
|
|
case 'unlike':
|
|
|
|
|
|
handleLike(post);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'comment':
|
|
|
|
|
|
handlePostPress(post.id, true);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'bookmark':
|
|
|
|
|
|
case 'unbookmark':
|
|
|
|
|
|
handleBookmark(post);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'share':
|
|
|
|
|
|
handleShare(post);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'imagePress':
|
|
|
|
|
|
if (action.payload?.images && action.payload?.imageIndex !== undefined) {
|
|
|
|
|
|
handleImagePress(action.payload.images, action.payload.imageIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'delete':
|
|
|
|
|
|
handleDeletePost(post.id);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-24 05:52:24 +08:00
|
|
|
|
const handlePostActionRef = useRef(handlePostAction);
|
|
|
|
|
|
handlePostActionRef.current = handlePostAction;
|
|
|
|
|
|
|
|
|
|
|
|
const stableOnPostAction = useCallback((postId: string, action: PostCardAction) => {
|
|
|
|
|
|
const post = postByIdRef.current.get(postId);
|
|
|
|
|
|
if (post) {
|
|
|
|
|
|
handlePostActionRef.current(post, action);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-18 00:25:46 +08:00
|
|
|
|
// 跳转到发帖页面(使用 Modal 方式)
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const handleCreatePost = () => {
|
2026-03-18 00:25:46 +08:00
|
|
|
|
setShowCreatePost(true);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-24 22:27:33 +08:00
|
|
|
|
const renderLatestCapsules = () => {
|
2026-03-24 23:02:24 +08:00
|
|
|
|
if (!isLatestTab) {
|
2026-03-24 22:27:33 +08:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={[styles.capsuleWrapper, { paddingHorizontal: responsivePadding }]}>
|
2026-03-25 01:29:41 +08:00
|
|
|
|
<ScrollView
|
|
|
|
|
|
ref={capsuleHScrollRef}
|
|
|
|
|
|
horizontal
|
|
|
|
|
|
showsHorizontalScrollIndicator={false}
|
|
|
|
|
|
contentContainerStyle={styles.capsuleList}
|
|
|
|
|
|
onScroll={onCapsuleHorizontalScroll}
|
|
|
|
|
|
scrollEventThrottle={16}
|
|
|
|
|
|
onContentSizeChange={restoreCapsuleStripScroll}
|
|
|
|
|
|
>
|
2026-03-24 22:27:33 +08:00
|
|
|
|
{latestCapsules.map((item) => {
|
|
|
|
|
|
const isActive = item.id === activeCapsuleId;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
key={item.id || 'all'}
|
|
|
|
|
|
activeOpacity={0.85}
|
|
|
|
|
|
onPress={() => setActiveCapsuleId(item.id)}
|
|
|
|
|
|
style={styles.capsuleItem}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
style={
|
|
|
|
|
|
isActive
|
|
|
|
|
|
? { ...styles.capsuleText, ...styles.capsuleTextActive }
|
|
|
|
|
|
: styles.capsuleText
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.name}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</ScrollView>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 渲染帖子卡片(列表模式)
|
2026-03-24 01:19:09 +08:00
|
|
|
|
const keyExtractor = useCallback((item: Post) => item.id, []);
|
|
|
|
|
|
|
|
|
|
|
|
const renderPostList = useCallback(({ item }: { item: Post }) => {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const authorId = item.author?.id || '';
|
|
|
|
|
|
const isPostAuthor = currentUser?.id === authorId;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={[
|
|
|
|
|
|
styles.listItem,
|
|
|
|
|
|
{
|
|
|
|
|
|
marginBottom: responsiveGap,
|
|
|
|
|
|
width: listItemWidth,
|
|
|
|
|
|
alignSelf: 'center',
|
|
|
|
|
|
borderRadius: isMobile ? borderRadius.lg : 0,
|
|
|
|
|
|
overflow: isMobile ? 'hidden' : 'visible',
|
|
|
|
|
|
}
|
|
|
|
|
|
]}>
|
|
|
|
|
|
<PostCard
|
|
|
|
|
|
post={item}
|
2026-03-24 05:52:24 +08:00
|
|
|
|
onAction={(action) => stableOnPostAction(item.id, action)}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
isPostAuthor={isPostAuthor}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
2026-03-24 05:52:24 +08:00
|
|
|
|
}, [currentUser?.id, stableOnPostAction, isMobile, listItemWidth, responsiveGap]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 估算帖子在瀑布流中的高度(用于均匀分配)
|
|
|
|
|
|
const estimatePostHeight = (post: Post, columnWidth: number): number => {
|
|
|
|
|
|
const hasImage = post.images && post.images.length > 0;
|
|
|
|
|
|
const hasTitle = !!post.title;
|
|
|
|
|
|
const hasContent = !!post.content;
|
|
|
|
|
|
|
|
|
|
|
|
let height = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 图片区域高度(如果有图)
|
|
|
|
|
|
if (hasImage) {
|
|
|
|
|
|
// 使用帖子 ID 生成一致的宽高比
|
|
|
|
|
|
const hash = post.id.split('').reduce((a, b) => a + b.charCodeAt(0), 0);
|
|
|
|
|
|
const aspectRatios = [0.7, 0.75, 0.8, 0.85, 0.9, 1, 1.1, 1.2];
|
|
|
|
|
|
const aspectRatio = aspectRatios[hash % aspectRatios.length];
|
|
|
|
|
|
height += columnWidth / aspectRatio;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 无图帖子显示正文预览区域
|
|
|
|
|
|
if (hasContent) {
|
|
|
|
|
|
// 根据内容长度估算高度(每行约20像素,最多6行)
|
|
|
|
|
|
const contentLength = post.content?.length || 0;
|
|
|
|
|
|
const estimatedLines = Math.min(6, Math.max(3, Math.ceil(contentLength / 20)));
|
|
|
|
|
|
height += 16 + estimatedLines * 20; // padding + 文本高度
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标题高度
|
|
|
|
|
|
if (hasTitle) {
|
|
|
|
|
|
const titleLines = hasImage ? 2 : 3;
|
|
|
|
|
|
height += 8 + titleLines * 20; // paddingTop + 文本高度
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 底部信息栏高度
|
|
|
|
|
|
height += 40; // 用户信息 + 点赞数
|
|
|
|
|
|
|
|
|
|
|
|
// 间距
|
|
|
|
|
|
height += 2; // marginBottom
|
|
|
|
|
|
|
|
|
|
|
|
return height;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 将帖子分成多列(瀑布流)- 使用贪心算法使各列高度尽量均匀
|
|
|
|
|
|
const distributePostsToColumns = useMemo(() => {
|
|
|
|
|
|
const columns: Post[][] = Array.from({ length: gridColumns }, () => []);
|
|
|
|
|
|
const columnHeights: number[] = Array(gridColumns).fill(0);
|
|
|
|
|
|
|
2026-03-21 03:22:28 +08:00
|
|
|
|
// 防御性检查:确保 posts 存在且是数组
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
if (!displayPosts || !Array.isArray(displayPosts) || displayPosts.length === 0) {
|
2026-03-21 03:22:28 +08:00
|
|
|
|
return columns;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 计算单列宽度
|
|
|
|
|
|
const totalGap = (gridColumns - 1) * responsiveGap;
|
|
|
|
|
|
const columnWidth = (width - responsivePadding * 2 - totalGap) / gridColumns;
|
|
|
|
|
|
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
displayPosts.forEach((post) => {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const postHeight = estimatePostHeight(post, columnWidth);
|
|
|
|
|
|
|
|
|
|
|
|
// 找到当前高度最小的列
|
|
|
|
|
|
const minHeightIndex = columnHeights.indexOf(Math.min(...columnHeights));
|
|
|
|
|
|
columns[minHeightIndex].push(post);
|
|
|
|
|
|
columnHeights[minHeightIndex] += postHeight;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return columns;
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
}, [displayPosts, gridColumns, width, responsiveGap, responsivePadding]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 渲染单列帖子
|
|
|
|
|
|
const renderWaterfallColumn = (column: Post[], columnIndex: number) => (
|
|
|
|
|
|
<View key={`column-${columnIndex}`} style={[styles.waterfallColumn, { marginRight: columnIndex < gridColumns - 1 ? responsiveGap : 0 }]}>
|
|
|
|
|
|
{column.map(post => {
|
|
|
|
|
|
const authorId = post.author?.id || '';
|
|
|
|
|
|
const isPostAuthor = currentUser?.id === authorId;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View key={post.id} style={[styles.waterfallItem, { marginBottom: responsiveGap }]}>
|
|
|
|
|
|
<PostCard
|
|
|
|
|
|
post={post}
|
|
|
|
|
|
variant="grid"
|
2026-03-24 05:52:24 +08:00
|
|
|
|
onAction={(action) => stableOnPostAction(post.id, action)}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
isPostAuthor={isPostAuthor}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染响应式网格布局(平板/桌面端使用多列)
|
|
|
|
|
|
const renderResponsiveGrid = () => {
|
|
|
|
|
|
if (isMobile && !isTablet) {
|
|
|
|
|
|
// 移动端使用瀑布流布局(2列)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ScrollView
|
2026-03-26 03:41:43 +08:00
|
|
|
|
ref={scrollViewRef}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
style={styles.waterfallScroll}
|
|
|
|
|
|
contentContainerStyle={[
|
|
|
|
|
|
styles.waterfallContainer,
|
|
|
|
|
|
{
|
|
|
|
|
|
paddingHorizontal: responsivePadding,
|
|
|
|
|
|
paddingBottom: 80 + responsivePadding,
|
|
|
|
|
|
}
|
|
|
|
|
|
]}
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
2026-03-25 01:30:00 +08:00
|
|
|
|
scrollEventThrottle={16}
|
2026-03-23 03:58:26 +08:00
|
|
|
|
onScroll={handleGridScroll}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
2026-03-20 23:00:27 +08:00
|
|
|
|
refreshing={isRefreshing}
|
|
|
|
|
|
onRefresh={refresh}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-03-24 22:27:33 +08:00
|
|
|
|
{renderLatestCapsules()}
|
2026-03-24 14:21:31 +08:00
|
|
|
|
<View style={styles.waterfallColumnsRow}>
|
|
|
|
|
|
{distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{/* 独立底部加载区:避免参与横向列布局导致列宽抖动 */}
|
|
|
|
|
|
{isLoadingMore && displayPosts.length > 0 && (
|
|
|
|
|
|
<View style={styles.loadingMoreFooter}>
|
2026-03-23 03:58:26 +08:00
|
|
|
|
<Loading size="sm" />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</ScrollView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 平板/桌面端使用 ResponsiveGrid 组件
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ResponsiveGrid
|
|
|
|
|
|
columns={{ xs: 1, sm: 2, md: 2, lg: 3, xl: 3, '2xl': 4, '3xl': 4, '4xl': 5 }}
|
|
|
|
|
|
gap={{ xs: 8, sm: 12, md: 16, lg: 20, xl: 24 }}
|
|
|
|
|
|
containerStyle={{ paddingHorizontal: responsivePadding, paddingBottom: 80 }}
|
|
|
|
|
|
>
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
{displayPosts.map(post => {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const authorId = post.author?.id || '';
|
|
|
|
|
|
const isPostAuthor = currentUser?.id === authorId;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PostCard
|
|
|
|
|
|
key={post.id}
|
|
|
|
|
|
post={post}
|
2026-03-24 04:23:13 +08:00
|
|
|
|
variant={viewMode === 'grid' ? 'grid' : 'list'}
|
2026-03-24 05:52:24 +08:00
|
|
|
|
onAction={(action) => stableOnPostAction(post.id, action)}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
isPostAuthor={isPostAuthor}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</ResponsiveGrid>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染空状态
|
2026-03-24 01:19:09 +08:00
|
|
|
|
const renderEmpty = useCallback(() => {
|
|
|
|
|
|
if (isInitialLoading) return null;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="暂无内容"
|
|
|
|
|
|
description={activeIndex === 1 ? '关注一些用户来获取内容吧' : '暂无帖子,快去发布第一条内容吧'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
2026-03-24 01:19:09 +08:00
|
|
|
|
}, [activeIndex, isInitialLoading]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 渲染列表内容
|
|
|
|
|
|
const renderListContent = () => {
|
|
|
|
|
|
if (useMultiColumnList) {
|
|
|
|
|
|
// 平板/桌面端使用多列网格
|
|
|
|
|
|
return renderResponsiveGrid();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 移动端和宽屏都使用单列 FlatList,宽屏下居中显示
|
|
|
|
|
|
return (
|
|
|
|
|
|
<FlatList
|
2026-03-26 03:41:43 +08:00
|
|
|
|
ref={flatListRef}
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
data={displayPosts}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
renderItem={renderPostList}
|
2026-03-24 01:19:09 +08:00
|
|
|
|
keyExtractor={keyExtractor}
|
2026-03-24 22:27:33 +08:00
|
|
|
|
ListHeaderComponent={renderLatestCapsules()}
|
2026-03-24 23:02:24 +08:00
|
|
|
|
ListHeaderComponentStyle={styles.listHeader}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
contentContainerStyle={[
|
|
|
|
|
|
styles.listContent,
|
|
|
|
|
|
{
|
|
|
|
|
|
paddingHorizontal: listHorizontalPadding,
|
|
|
|
|
|
paddingBottom: 80 + responsivePadding,
|
|
|
|
|
|
alignItems: 'center', // 确保子项居中
|
|
|
|
|
|
}
|
|
|
|
|
|
]}
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
2026-03-20 23:00:27 +08:00
|
|
|
|
refreshing={isRefreshing}
|
|
|
|
|
|
onRefresh={refresh}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
2026-03-20 23:00:27 +08:00
|
|
|
|
onEndReached={loadMore}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
onEndReachedThreshold={0.3}
|
2026-03-25 01:30:00 +08:00
|
|
|
|
onScroll={handleHomeVerticalScroll}
|
2026-03-24 22:27:33 +08:00
|
|
|
|
scrollEventThrottle={16}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
ListEmptyComponent={renderEmpty}
|
2026-03-24 01:19:09 +08:00
|
|
|
|
ListFooterComponent={isLoadingMore ? <Loading size="sm" /> : null}
|
|
|
|
|
|
initialNumToRender={8}
|
|
|
|
|
|
maxToRenderPerBatch={8}
|
|
|
|
|
|
updateCellsBatchingPeriod={60}
|
|
|
|
|
|
windowSize={7}
|
2026-03-24 23:02:24 +08:00
|
|
|
|
removeClippedSubviews={false}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-16 17:47:10 +08:00
|
|
|
|
// 搜索页面内嵌模式
|
|
|
|
|
|
if (showSearch) {
|
|
|
|
|
|
return (
|
2026-03-18 10:58:41 +08:00
|
|
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
2026-03-25 05:16:54 +08:00
|
|
|
|
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.paper} />
|
2026-03-16 17:47:10 +08:00
|
|
|
|
<SearchScreen
|
|
|
|
|
|
onBack={() => setShowSearch(false)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 正常首页内容
|
2026-03-09 21:29:03 +08:00
|
|
|
|
return (
|
2026-03-18 10:58:41 +08:00
|
|
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
2026-03-25 05:16:54 +08:00
|
|
|
|
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.paper} />
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
{/* 顶部Header */}
|
|
|
|
|
|
<View style={styles.header}>
|
|
|
|
|
|
{/* 搜索栏 */}
|
|
|
|
|
|
<View style={[styles.searchWrapper, { paddingHorizontal: responsivePadding }]}>
|
|
|
|
|
|
<SearchBar
|
|
|
|
|
|
value=""
|
|
|
|
|
|
onChangeText={() => {}}
|
|
|
|
|
|
onSubmit={handleSearchPress}
|
|
|
|
|
|
onFocus={handleSearchPress}
|
|
|
|
|
|
placeholder="搜索帖子、用户"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Tab切换 */}
|
|
|
|
|
|
<TabBar
|
|
|
|
|
|
tabs={TABS}
|
|
|
|
|
|
icons={TAB_ICONS}
|
|
|
|
|
|
activeIndex={activeIndex}
|
|
|
|
|
|
onTabChange={changeTab}
|
|
|
|
|
|
variant="modern"
|
2026-03-25 01:30:00 +08:00
|
|
|
|
style={styles.homeTabBar}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
rightContent={
|
|
|
|
|
|
<TouchableOpacity onPress={toggleViewMode} style={styles.viewToggleBtn}>
|
|
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
|
name={viewMode === 'list' ? 'view-grid-outline' : 'view-list'}
|
|
|
|
|
|
size={22}
|
|
|
|
|
|
color="#666"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 帖子列表 */}
|
2026-03-16 17:47:10 +08:00
|
|
|
|
{isMobile ? (
|
|
|
|
|
|
// 移动端:使用 GestureDetector 支持手势切换 Tab
|
|
|
|
|
|
<GestureDetector gesture={swipeGesture}>
|
|
|
|
|
|
<View style={styles.contentContainer}>
|
2026-03-24 01:19:09 +08:00
|
|
|
|
{isInitialLoading ? (
|
|
|
|
|
|
<Loading fullScreen />
|
|
|
|
|
|
) : viewMode === 'list' ? (
|
2026-03-16 17:47:10 +08:00
|
|
|
|
renderListContent()
|
|
|
|
|
|
) : (
|
|
|
|
|
|
// 网格模式:使用响应式网格
|
|
|
|
|
|
renderResponsiveGrid()
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</GestureDetector>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
// 桌面端/平板端:不使用 GestureDetector,避免拦截侧边栏点击
|
2026-03-09 21:29:03 +08:00
|
|
|
|
<View style={styles.contentContainer}>
|
2026-03-24 01:19:09 +08:00
|
|
|
|
{isInitialLoading ? (
|
|
|
|
|
|
<Loading fullScreen />
|
|
|
|
|
|
) : viewMode === 'list' ? (
|
2026-03-09 21:29:03 +08:00
|
|
|
|
renderListContent()
|
|
|
|
|
|
) : (
|
|
|
|
|
|
// 网格模式:使用响应式网格
|
|
|
|
|
|
renderResponsiveGrid()
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
2026-03-16 17:47:10 +08:00
|
|
|
|
)}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 漂浮发帖按钮 */}
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.floatingButton,
|
|
|
|
|
|
isDesktop && styles.floatingButtonDesktop,
|
|
|
|
|
|
isWideScreen && styles.floatingButtonWide,
|
|
|
|
|
|
floatingButtonBottom !== undefined && { bottom: floatingButtonBottom },
|
|
|
|
|
|
]}
|
|
|
|
|
|
onPress={handleCreatePost}
|
|
|
|
|
|
activeOpacity={0.8}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MaterialCommunityIcons name="plus" size={28} color={colors.text.inverse} />
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 图片查看器 */}
|
|
|
|
|
|
<ImageGallery
|
|
|
|
|
|
visible={showImageViewer}
|
2026-03-16 17:47:10 +08:00
|
|
|
|
images={postImages.map(img => ({
|
|
|
|
|
|
id: img.id || img.url || String(Math.random()),
|
|
|
|
|
|
url: img.url || img.uri || ''
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}))}
|
|
|
|
|
|
initialIndex={selectedImageIndex}
|
|
|
|
|
|
onClose={() => setShowImageViewer(false)}
|
|
|
|
|
|
enableSave
|
|
|
|
|
|
/>
|
2026-03-18 00:25:46 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 发帖弹窗 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
visible={showCreatePost}
|
|
|
|
|
|
animationType="slide"
|
|
|
|
|
|
presentationStyle="pageSheet"
|
|
|
|
|
|
onRequestClose={() => setShowCreatePost(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<CreatePostScreen
|
|
|
|
|
|
onClose={() => setShowCreatePost(false)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Modal>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|