refactor(home): redesign home screen with sort bar and add recommendation feed
- Replace PagerView-based tabs with modern sort bar navigation - Add "推荐" (recommended) feed option with "hot" sort type - Update tab icons and labels to match new sort paradigm - Optimize PostCard, MessageBubble conditional styles perf(chat): migrate FlatList to FlashList for emoji/sticker panels - Improve virtualized rendering performance for emoji and sticker grids - Fix jump-to-latest scroll behavior for inverted FlashList perf(profile): extract user header and tab bar into separate memoized renders - Prevent unnecessary re-renders when switching tabs feat(api): add channel_id filter support for post queries - Include channel_id in cursor pagination requests - Update CursorPaginationRequest type definition style(chat-info): redesign group and private chat info screens with flat layout - Remove card borders and shadows for cleaner appearance - Adjust avatar sizes and spacing for consistency
This commit is contained in:
@@ -605,7 +605,7 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
|||||||
size={14}
|
size={14}
|
||||||
color={post.is_liked ? colors.primary.main : colors.text.secondary}
|
color={post.is_liked ? colors.primary.main : colors.text.secondary}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.gridLikeCount, post.is_liked && { color: colors.primary.main }]}>
|
<Text style={[styles.gridLikeCount, post.is_liked ? { color: colors.primary.main } : {}]}>
|
||||||
{formatNumber(post.likes_count || 0)}
|
{formatNumber(post.likes_count || 0)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ import { FlashList, ListRenderItem, FlashListRef } from '@shopify/flash-list';
|
|||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useRouter, useFocusEffect } from 'expo-router';
|
import { useRouter, useFocusEffect } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { PagerView } from '../../components/common';
|
|
||||||
import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme';
|
import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme';
|
||||||
import { Post } from '../../types';
|
import { Post } from '../../types';
|
||||||
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
|
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
|
||||||
import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth';
|
import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth';
|
||||||
import { channelService, postService } from '../../services';
|
import { channelService, postService } from '../../services';
|
||||||
import { PostCard, TabBar, SearchBar, ShareSheet } from '../../components/business';
|
import { PostCard, SearchBar, ShareSheet } from '../../components/business';
|
||||||
import type { PostCardAction } from '../../components/business/PostCard';
|
import type { PostCardAction } from '../../components/business/PostCard';
|
||||||
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common';
|
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common';
|
||||||
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||||
@@ -40,8 +40,9 @@ import { CreatePostScreen } from '../create/CreatePostScreen';
|
|||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { blurActiveElement } from '../../infrastructure/platform';
|
import { blurActiveElement } from '../../infrastructure/platform';
|
||||||
|
|
||||||
const TABS = ['关注', '最新', '热门'];
|
const SORT_OPTIONS = ['关注', '推荐', '最新'];
|
||||||
const TAB_ICONS = ['account-heart-outline', 'clock-outline', 'fire'];
|
const SORT_ICONS = ['account-heart', 'fire', 'clock-outline'];
|
||||||
|
const SORT_TYPES: PostType[] = ['follow', 'hot', 'latest'];
|
||||||
const DEFAULT_PAGE_SIZE = 20;
|
const DEFAULT_PAGE_SIZE = 20;
|
||||||
const MOBILE_TAB_BAR_HEIGHT = 64;
|
const MOBILE_TAB_BAR_HEIGHT = 64;
|
||||||
const MOBILE_TAB_FLOATING_MARGIN = 12;
|
const MOBILE_TAB_FLOATING_MARGIN = 12;
|
||||||
@@ -83,13 +84,20 @@ function createHomeStyles(colors: AppColors) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
capsuleWrapper: {
|
sortBar: {
|
||||||
paddingBottom: spacing.sm,
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingVertical: spacing.xs,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
capsuleList: {
|
sortBarCapsuleScroll: {
|
||||||
|
flex: 1,
|
||||||
|
marginHorizontal: spacing.xs,
|
||||||
|
},
|
||||||
|
sortBarCapsuleContent: {
|
||||||
gap: spacing.xs,
|
gap: spacing.xs,
|
||||||
paddingRight: spacing.lg,
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
capsuleItem: {
|
capsuleItem: {
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
@@ -104,12 +112,21 @@ function createHomeStyles(colors: AppColors) {
|
|||||||
capsuleTextActive: {
|
capsuleTextActive: {
|
||||||
color: colors.primary.main,
|
color: colors.primary.main,
|
||||||
},
|
},
|
||||||
|
sortTrigger: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 4,
|
||||||
|
paddingHorizontal: 2,
|
||||||
|
},
|
||||||
|
sortTriggerText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text.secondary,
|
||||||
|
marginLeft: 2,
|
||||||
|
},
|
||||||
listContent: {
|
listContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
},
|
},
|
||||||
listHeader: {
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -181,7 +198,7 @@ export const HomeScreen: React.FC = () => {
|
|||||||
const responsiveGap = useResponsiveSpacing({ xs: 4, sm: 6, md: 8, lg: 12, xl: 16 });
|
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 });
|
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||||||
|
|
||||||
const [activeIndex, setActiveIndex] = useState(1);
|
const [sortIndex, setSortIndex] = useState(0);
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||||
const [latestCapsules, setLatestCapsules] = useState<LatestCapsule[]>([{ id: '', name: '全部' }]);
|
const [latestCapsules, setLatestCapsules] = useState<LatestCapsule[]>([{ id: '', name: '全部' }]);
|
||||||
const [activeCapsuleId, setActiveCapsuleId] = useState('');
|
const [activeCapsuleId, setActiveCapsuleId] = useState('');
|
||||||
@@ -218,9 +235,6 @@ export const HomeScreen: React.FC = () => {
|
|||||||
|
|
||||||
const isLoadingMoreRef = useRef(false);
|
const isLoadingMoreRef = useRef(false);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const pagerRef = useRef<any>(null);
|
|
||||||
|
|
||||||
const setBottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.setBottomTabBarHiddenByScroll);
|
const setBottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.setBottomTabBarHiddenByScroll);
|
||||||
const bottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.bottomTabBarHiddenByScroll);
|
const bottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.bottomTabBarHiddenByScroll);
|
||||||
const homeTabPressCount = useHomeTabPressStore((s) => s.pressCount);
|
const homeTabPressCount = useHomeTabPressStore((s) => s.pressCount);
|
||||||
@@ -325,28 +339,28 @@ export const HomeScreen: React.FC = () => {
|
|||||||
return map;
|
return map;
|
||||||
}, [storePosts]);
|
}, [storePosts]);
|
||||||
|
|
||||||
// 获取当前 tab 对应的帖子类型
|
// 获取当前排序对应的帖子类型
|
||||||
const getPostType = useCallback((): PostType => {
|
const getPostType = useCallback((): PostType => {
|
||||||
switch (activeIndex) {
|
return SORT_TYPES[sortIndex] ?? 'hot';
|
||||||
case 0: return 'follow';
|
}, [sortIndex]);
|
||||||
case 1: return 'latest';
|
|
||||||
case 2: return 'hot';
|
|
||||||
default: return 'latest';
|
|
||||||
}
|
|
||||||
}, [activeIndex]);
|
|
||||||
|
|
||||||
const isLatestTab = activeIndex === 1;
|
const currentChannelId = activeCapsuleId || undefined;
|
||||||
const currentChannelId = isLatestTab && activeCapsuleId ? activeCapsuleId : undefined;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
homeListScrollYRef.current = 0;
|
homeListScrollYRef.current = 0;
|
||||||
setBottomTabBarHiddenByScroll(false);
|
setBottomTabBarHiddenByScroll(false);
|
||||||
}, [activeIndex, currentChannelId, viewMode, setBottomTabBarHiddenByScroll]);
|
}, [sortIndex, currentChannelId, setBottomTabBarHiddenByScroll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
flashListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
||||||
|
scrollViewRef.current?.scrollTo({ y: 0, animated: false });
|
||||||
|
});
|
||||||
|
}, [viewMode]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!isLatestTab) return;
|
|
||||||
restoreCapsuleStripScroll();
|
restoreCapsuleStripScroll();
|
||||||
}, [isLatestTab, activeCapsuleId, latestCapsules.length, restoreCapsuleStripScroll]);
|
}, [activeCapsuleId, latestCapsules.length, restoreCapsuleStripScroll]);
|
||||||
|
|
||||||
// 使用差异更新 Hook 获取帖子列表
|
// 使用差异更新 Hook 获取帖子列表
|
||||||
const listKey = useMemo(
|
const listKey = useMemo(
|
||||||
@@ -419,13 +433,10 @@ export const HomeScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [listKey, getPostType, currentChannelId]);
|
}, [listKey, getPostType, currentChannelId]);
|
||||||
|
|
||||||
// Tab 切换时刷新数据并重置列表
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
reset();
|
reset();
|
||||||
// reset 会清空 store 中的状态,但异步订阅可能有延迟;
|
|
||||||
// 等一帧确保 store 的订阅回调已传播到 useDifferentialPosts
|
|
||||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
await refresh();
|
await refresh();
|
||||||
@@ -434,7 +445,7 @@ export const HomeScreen: React.FC = () => {
|
|||||||
run();
|
run();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [activeIndex, currentChannelId]);
|
}, [sortIndex, currentChannelId]);
|
||||||
|
|
||||||
// 将获取到的原始帖子与 store 中的状态合并
|
// 将获取到的原始帖子与 store 中的状态合并
|
||||||
// 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等)
|
// 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等)
|
||||||
@@ -511,23 +522,13 @@ export const HomeScreen: React.FC = () => {
|
|||||||
setViewMode(prev => prev === 'list' ? 'grid' : 'list');
|
setViewMode(prev => prev === 'list' ? 'grid' : 'list');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换Tab(点击 TabBar 或手势滑动)
|
// 切换排序
|
||||||
const changeTab = useCallback((nextIndex: number) => {
|
const changeSort = useCallback((nextIndex: number) => {
|
||||||
if (nextIndex < 0 || nextIndex >= TABS.length || nextIndex === activeIndex) {
|
if (nextIndex < 0 || nextIndex >= SORT_OPTIONS.length || nextIndex === sortIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setActiveIndex(nextIndex);
|
setSortIndex(nextIndex);
|
||||||
if (Platform.OS !== 'web') {
|
}, [sortIndex]);
|
||||||
pagerRef.current?.setPage(nextIndex);
|
|
||||||
}
|
|
||||||
}, [activeIndex]);
|
|
||||||
|
|
||||||
const onPagerPageSelected = useCallback((e: { nativeEvent: { position: number } }) => {
|
|
||||||
const nextIndex = e.nativeEvent.position;
|
|
||||||
if (nextIndex !== activeIndex) {
|
|
||||||
setActiveIndex(nextIndex);
|
|
||||||
}
|
|
||||||
}, [activeIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadChannels = async () => {
|
const loadChannels = async () => {
|
||||||
@@ -669,46 +670,41 @@ export const HomeScreen: React.FC = () => {
|
|||||||
setShowCreatePost(true);
|
setShowCreatePost(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLatestCapsules = () => {
|
const renderChannelCapsules = () => {
|
||||||
if (!isLatestTab) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.capsuleWrapper, { paddingHorizontal: responsivePadding }]}>
|
<ScrollView
|
||||||
<ScrollView
|
ref={capsuleHScrollRef}
|
||||||
ref={capsuleHScrollRef}
|
horizontal
|
||||||
horizontal
|
showsHorizontalScrollIndicator={false}
|
||||||
showsHorizontalScrollIndicator={false}
|
nestedScrollEnabled
|
||||||
nestedScrollEnabled
|
style={styles.sortBarCapsuleScroll}
|
||||||
contentContainerStyle={styles.capsuleList}
|
contentContainerStyle={styles.sortBarCapsuleContent}
|
||||||
onScroll={onCapsuleHorizontalScroll}
|
onScroll={onCapsuleHorizontalScroll}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
onContentSizeChange={restoreCapsuleStripScroll}
|
onContentSizeChange={restoreCapsuleStripScroll}
|
||||||
>
|
>
|
||||||
{latestCapsules.map((item) => {
|
{latestCapsules.map((item) => {
|
||||||
const isActive = item.id === activeCapsuleId;
|
const isActive = item.id === activeCapsuleId;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={item.id || 'all'}
|
key={item.id || 'all'}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
onPress={() => setActiveCapsuleId(item.id)}
|
onPress={() => setActiveCapsuleId(item.id)}
|
||||||
style={styles.capsuleItem}
|
style={styles.capsuleItem}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
isActive
|
||||||
|
? { ...styles.capsuleText, ...styles.capsuleTextActive }
|
||||||
|
: styles.capsuleText
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Text
|
{item.name}
|
||||||
style={
|
</Text>
|
||||||
isActive
|
</TouchableOpacity>
|
||||||
? { ...styles.capsuleText, ...styles.capsuleTextActive }
|
);
|
||||||
: styles.capsuleText
|
})}
|
||||||
}
|
</ScrollView>
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -758,6 +754,7 @@ export const HomeScreen: React.FC = () => {
|
|||||||
const renderResponsiveGrid = () => {
|
const renderResponsiveGrid = () => {
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
|
key={`${listKey}_grid`}
|
||||||
ref={scrollViewRef as any}
|
ref={scrollViewRef as any}
|
||||||
data={displayPosts}
|
data={displayPosts}
|
||||||
renderItem={renderGridItem}
|
renderItem={renderGridItem}
|
||||||
@@ -765,8 +762,6 @@ export const HomeScreen: React.FC = () => {
|
|||||||
numColumns={gridColumns}
|
numColumns={gridColumns}
|
||||||
masonry
|
masonry
|
||||||
optimizeItemArrangement
|
optimizeItemArrangement
|
||||||
ListHeaderComponent={renderLatestCapsules()}
|
|
||||||
ListHeaderComponentStyle={styles.listHeader}
|
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingHorizontal: responsivePadding,
|
paddingHorizontal: responsivePadding,
|
||||||
paddingBottom: 80 + responsivePadding,
|
paddingBottom: 80 + responsivePadding,
|
||||||
@@ -798,10 +793,10 @@ export const HomeScreen: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="暂无内容"
|
title="暂无内容"
|
||||||
description={activeIndex === 1 ? '关注一些用户来获取内容吧' : '暂无帖子,快去发布第一条内容吧'}
|
description="暂无帖子,快去发布第一条内容吧"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [activeIndex, isInitialLoading]);
|
}, [isInitialLoading]);
|
||||||
|
|
||||||
// 渲染列表内容
|
// 渲染列表内容
|
||||||
const renderListContent = () => {
|
const renderListContent = () => {
|
||||||
@@ -813,12 +808,11 @@ export const HomeScreen: React.FC = () => {
|
|||||||
// 移动端和宽屏都使用单列 FlashList,宽屏下居中显示
|
// 移动端和宽屏都使用单列 FlashList,宽屏下居中显示
|
||||||
return (
|
return (
|
||||||
<FlashList
|
<FlashList
|
||||||
|
key={listKey}
|
||||||
ref={flashListRef}
|
ref={flashListRef}
|
||||||
data={displayPosts}
|
data={displayPosts}
|
||||||
renderItem={renderPostList}
|
renderItem={renderPostList}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
ListHeaderComponent={renderLatestCapsules()}
|
|
||||||
ListHeaderComponentStyle={styles.listHeader}
|
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingHorizontal: listHorizontalPadding,
|
paddingHorizontal: listHorizontalPadding,
|
||||||
paddingBottom: 80 + responsivePadding,
|
paddingBottom: 80 + responsivePadding,
|
||||||
@@ -873,74 +867,45 @@ export const HomeScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tab切换 */}
|
{/* 排序筛选 + 频道 */}
|
||||||
<TabBar
|
<View style={styles.sortBar}>
|
||||||
tabs={TABS}
|
<TouchableOpacity onPress={toggleViewMode} style={styles.viewToggleBtn}>
|
||||||
icons={TAB_ICONS}
|
<MaterialCommunityIcons
|
||||||
activeIndex={activeIndex}
|
name={viewMode === 'list' ? 'view-grid-outline' : 'view-list'}
|
||||||
onTabChange={changeTab}
|
size={22}
|
||||||
variant="modern"
|
color="#666"
|
||||||
style={styles.homeTabBar}
|
/>
|
||||||
rightContent={
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={toggleViewMode} style={styles.viewToggleBtn}>
|
|
||||||
<MaterialCommunityIcons
|
{renderChannelCapsules()}
|
||||||
name={viewMode === 'list' ? 'view-grid-outline' : 'view-list'}
|
|
||||||
size={22}
|
<TouchableOpacity
|
||||||
color="#666"
|
activeOpacity={0.7}
|
||||||
/>
|
onPress={() => changeSort((sortIndex + 1) % SORT_OPTIONS.length)}
|
||||||
</TouchableOpacity>
|
style={styles.sortTrigger}
|
||||||
}
|
>
|
||||||
/>
|
<MaterialCommunityIcons
|
||||||
|
name={SORT_ICONS[sortIndex] as any}
|
||||||
|
size={18}
|
||||||
|
color={colors.text.secondary}
|
||||||
|
/>
|
||||||
|
<Text style={styles.sortTriggerText}>
|
||||||
|
{SORT_OPTIONS[sortIndex]}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 帖子列表 */}
|
{/* 帖子列表 */}
|
||||||
{Platform.OS === 'web' ? (
|
<View style={styles.contentContainer}>
|
||||||
<View style={styles.contentContainer}>
|
{isInitialLoading ? (
|
||||||
{isInitialLoading ? (
|
<Loading fullScreen />
|
||||||
<Loading fullScreen />
|
) : viewMode === 'list' ? (
|
||||||
) : viewMode === 'list' ? (
|
renderListContent()
|
||||||
renderListContent()
|
) : (
|
||||||
) : (
|
renderResponsiveGrid()
|
||||||
renderResponsiveGrid()
|
)}
|
||||||
)}
|
</View>
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<PagerView
|
|
||||||
ref={pagerRef}
|
|
||||||
style={styles.contentContainer}
|
|
||||||
initialPage={activeIndex}
|
|
||||||
onPageSelected={onPagerPageSelected}
|
|
||||||
overdrag
|
|
||||||
>
|
|
||||||
<View key="follow" collapsable={false} style={styles.contentContainer}>
|
|
||||||
{isInitialLoading && activeIndex === 0 ? (
|
|
||||||
<Loading fullScreen />
|
|
||||||
) : activeIndex === 0 ? (
|
|
||||||
viewMode === 'list' ? renderListContent() : renderResponsiveGrid()
|
|
||||||
) : (
|
|
||||||
<View style={styles.contentContainer} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View key="latest" collapsable={false} style={styles.contentContainer}>
|
|
||||||
{isInitialLoading && activeIndex === 1 ? (
|
|
||||||
<Loading fullScreen />
|
|
||||||
) : activeIndex === 1 ? (
|
|
||||||
viewMode === 'list' ? renderListContent() : renderResponsiveGrid()
|
|
||||||
) : (
|
|
||||||
<View style={styles.contentContainer} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View key="hot" collapsable={false} style={styles.contentContainer}>
|
|
||||||
{isInitialLoading && activeIndex === 2 ? (
|
|
||||||
<Loading fullScreen />
|
|
||||||
) : activeIndex === 2 ? (
|
|
||||||
viewMode === 'list' ? renderListContent() : renderResponsiveGrid()
|
|
||||||
) : (
|
|
||||||
<View style={styles.contentContainer} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</PagerView>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 漂浮发帖按钮 */}
|
{/* 漂浮发帖按钮 */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|||||||
@@ -608,12 +608,12 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
{/* 群组基本信息卡片 */}
|
{/* 群组基本信息 */}
|
||||||
<View style={styles.headerCard}>
|
<View style={styles.headerCard}>
|
||||||
<View style={styles.groupHeader}>
|
<View style={styles.groupHeader}>
|
||||||
<Avatar
|
<Avatar
|
||||||
source={group.avatar}
|
source={group.avatar}
|
||||||
size={90}
|
size={80}
|
||||||
name={group.name}
|
name={group.name}
|
||||||
/>
|
/>
|
||||||
<View style={styles.groupInfo}>
|
<View style={styles.groupInfo}>
|
||||||
@@ -621,7 +621,7 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
{group.name}
|
{group.name}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.groupMeta}>
|
<View style={styles.groupMeta}>
|
||||||
<MaterialCommunityIcons name="account-group" size={16} color={colors.text.secondary} />
|
<MaterialCommunityIcons name="account-group" size={14} color={colors.text.secondary} />
|
||||||
<Text variant="caption" color={colors.text.secondary}>
|
<Text variant="caption" color={colors.text.secondary}>
|
||||||
{group.member_count} 人
|
{group.member_count} 人
|
||||||
</Text>
|
</Text>
|
||||||
@@ -661,7 +661,7 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 群公告卡片 */}
|
{/* 群公告 */}
|
||||||
{announcements.length > 0 && (
|
{announcements.length > 0 && (
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
@@ -684,7 +684,7 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 成员预览卡片 */}
|
{/* 成员预览 */}
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<TouchableOpacity style={styles.cardHeader} onPress={goToMembers} activeOpacity={0.7}>
|
<TouchableOpacity style={styles.cardHeader} onPress={goToMembers} activeOpacity={0.7}>
|
||||||
<View style={styles.cardIconContainer}>
|
<View style={styles.cardIconContainer}>
|
||||||
@@ -813,6 +813,7 @@ const GroupInfoScreen: React.FC = () => {
|
|||||||
thumbColor={isPinned ? colors.primary.main : colors.background.paper}
|
thumbColor={isPinned ? colors.primary.main : colors.background.paper}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<Divider style={styles.settingDivider} />
|
||||||
<View style={styles.settingItem}>
|
<View style={styles.settingItem}>
|
||||||
<View style={styles.settingIconContainer}>
|
<View style={styles.settingIconContainer}>
|
||||||
<MaterialCommunityIcons name="bell-off-outline" size={20} color={colors.primary.main} />
|
<MaterialCommunityIcons name="bell-off-outline" size={20} color={colors.primary.main} />
|
||||||
@@ -1126,22 +1127,20 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
pageHeader: {
|
pageHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomWidth: 0.5,
|
borderBottomWidth: 0,
|
||||||
borderBottomColor: colors.divider + '60',
|
|
||||||
},
|
},
|
||||||
pageTitle: {
|
pageTitle: {
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
fontSize: fontSizes.xl,
|
fontSize: fontSizes.xl,
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
},
|
||||||
pageHeaderPlaceholder: {
|
pageHeaderPlaceholder: {
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -1151,7 +1150,7 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
padding: spacing.md,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingBottom: spacing.xl * 2,
|
paddingBottom: spacing.xl * 2,
|
||||||
},
|
},
|
||||||
errorContainer: {
|
errorContainer: {
|
||||||
@@ -1160,29 +1159,25 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 头部卡片样式 - 扁平化设计
|
// 头部区域 - 扁平化无卡片
|
||||||
headerCard: {
|
headerCard: {
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderRadius: borderRadius.xl,
|
paddingVertical: spacing.lg,
|
||||||
padding: spacing.lg,
|
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
borderWidth: 0.5,
|
|
||||||
borderColor: colors.divider + '40',
|
|
||||||
},
|
},
|
||||||
groupHeader: {
|
groupHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.lg,
|
||||||
},
|
},
|
||||||
groupInfo: {
|
groupInfo: {
|
||||||
marginLeft: spacing.lg,
|
marginLeft: spacing.lg,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
groupName: {
|
groupName: {
|
||||||
fontWeight: '800',
|
fontWeight: '700',
|
||||||
fontSize: fontSizes.xl + 2,
|
fontSize: fontSizes['2xl'],
|
||||||
marginBottom: spacing.xs,
|
marginBottom: spacing.xs,
|
||||||
letterSpacing: 0.3,
|
|
||||||
},
|
},
|
||||||
groupMeta: {
|
groupMeta: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -1213,8 +1208,6 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.lg,
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
borderWidth: 0.5,
|
|
||||||
borderColor: colors.divider + '30',
|
|
||||||
},
|
},
|
||||||
groupDesc: {
|
groupDesc: {
|
||||||
marginLeft: spacing.sm,
|
marginLeft: spacing.sm,
|
||||||
@@ -1223,19 +1216,16 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 通用卡片样式 - 扁平化
|
// 通用卡片样式 - 扁平化:无阴影无圆角,使用分割线
|
||||||
card: {
|
card: {
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderRadius: borderRadius.xl,
|
|
||||||
padding: spacing.lg,
|
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
borderWidth: 0.5,
|
|
||||||
borderColor: colors.divider + '40',
|
|
||||||
},
|
},
|
||||||
cardHeader: {
|
cardHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
|
paddingTop: spacing.sm,
|
||||||
},
|
},
|
||||||
cardIconContainer: {
|
cardIconContainer: {
|
||||||
width: 36,
|
width: 36,
|
||||||
@@ -1247,13 +1237,12 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
marginRight: spacing.sm,
|
marginRight: spacing.sm,
|
||||||
},
|
},
|
||||||
cardTitle: {
|
cardTitle: {
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
fontSize: fontSizes.md + 1,
|
fontSize: fontSizes.md,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
letterSpacing: 0.3,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 公告样式 - 更现代的卡片
|
// 公告样式 - 扁平化
|
||||||
announcementContent: {
|
announcementContent: {
|
||||||
backgroundColor: colors.warning.light + '08',
|
backgroundColor: colors.warning.light + '08',
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.lg,
|
||||||
@@ -1324,7 +1313,7 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
fontSize: fontSizes.sm,
|
fontSize: fontSizes.sm,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 邀请按钮样式 - 更加醒目
|
// 邀请按钮样式 - 扁平化
|
||||||
inviteButton: {
|
inviteButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -1348,25 +1337,23 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
inviteButtonText: {
|
inviteButtonText: {
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
marginRight: spacing.xs,
|
marginRight: spacing.xs,
|
||||||
letterSpacing: 0.3,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 设置项样式 - 更加扁平
|
// 设置项样式 - 扁平化:无圆角背景,简洁分割线
|
||||||
settingsList: {
|
settingsList: {
|
||||||
marginTop: spacing.sm,
|
marginTop: 0,
|
||||||
},
|
},
|
||||||
settingItem: {
|
settingItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
borderRadius: borderRadius.md,
|
paddingHorizontal: 0,
|
||||||
paddingHorizontal: spacing.sm,
|
marginLeft: 0,
|
||||||
marginLeft: -spacing.sm,
|
marginRight: 0,
|
||||||
marginRight: -spacing.sm,
|
|
||||||
},
|
},
|
||||||
settingIconContainer: {
|
settingIconContainer: {
|
||||||
width: 40,
|
width: 36,
|
||||||
height: 40,
|
height: 36,
|
||||||
borderRadius: borderRadius.md,
|
borderRadius: borderRadius.md,
|
||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.default,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -1384,18 +1371,16 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
settingDivider: {
|
settingDivider: {
|
||||||
marginLeft: 56,
|
marginLeft: 52,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
marginVertical: spacing.xs,
|
marginVertical: 0,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 危险操作卡片 - 更加醒目
|
// 危险操作区域 - 扁平化
|
||||||
dangerCard: {
|
dangerCard: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: colors.error.light + '30',
|
|
||||||
marginTop: spacing.sm,
|
marginTop: spacing.sm,
|
||||||
},
|
},
|
||||||
dangerButton: {
|
dangerButton: {
|
||||||
@@ -1404,14 +1389,12 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: spacing.lg,
|
paddingVertical: spacing.lg,
|
||||||
gap: spacing.sm,
|
gap: spacing.sm,
|
||||||
backgroundColor: colors.error.light + '08',
|
|
||||||
},
|
},
|
||||||
dangerButtonText: {
|
dangerButtonText: {
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
letterSpacing: 0.3,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 模态框样式 - 更现代
|
// 模态框样式 - 扁平化
|
||||||
modalOverlay: {
|
modalOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.45)',
|
backgroundColor: 'rgba(0, 0, 0, 0.45)',
|
||||||
@@ -1440,20 +1423,18 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
},
|
},
|
||||||
modalTitle: {
|
modalTitle: {
|
||||||
fontWeight: '800',
|
fontWeight: '700',
|
||||||
fontSize: fontSizes.xl + 1,
|
fontSize: fontSizes.xl,
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
},
|
||||||
saveButton: {
|
saveButton: {
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
letterSpacing: 0.3,
|
|
||||||
},
|
},
|
||||||
confirmButton: {
|
confirmButton: {
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 编辑表单样式 - 更现代
|
// 编辑表单样式 - 扁平化
|
||||||
editForm: {
|
editForm: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
@@ -1465,7 +1446,7 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
width: 90,
|
width: 90,
|
||||||
height: 90,
|
height: 90,
|
||||||
borderRadius: 45,
|
borderRadius: 45,
|
||||||
borderWidth: 3,
|
borderWidth: 2,
|
||||||
borderColor: colors.primary.light + '40',
|
borderColor: colors.primary.light + '40',
|
||||||
},
|
},
|
||||||
editAvatarPlaceholder: {
|
editAvatarPlaceholder: {
|
||||||
@@ -1496,9 +1477,8 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
editLabel: {
|
editLabel: {
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
fontSize: fontSizes.sm + 1,
|
fontSize: fontSizes.sm,
|
||||||
letterSpacing: 0.3,
|
|
||||||
},
|
},
|
||||||
editInput: {
|
editInput: {
|
||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.default,
|
||||||
@@ -1507,7 +1487,7 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
paddingVertical: spacing.md,
|
paddingVertical: spacing.md,
|
||||||
fontSize: fontSizes.md,
|
fontSize: fontSizes.md,
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
borderWidth: 1.5,
|
borderWidth: 1,
|
||||||
borderColor: colors.divider + '80',
|
borderColor: colors.divider + '80',
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
},
|
},
|
||||||
@@ -1533,7 +1513,7 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
fontSize: fontSizes.md,
|
fontSize: fontSizes.md,
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
borderWidth: 1.5,
|
borderWidth: 1,
|
||||||
borderColor: colors.divider + '80',
|
borderColor: colors.divider + '80',
|
||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
textAlignVertical: 'top',
|
textAlignVertical: 'top',
|
||||||
@@ -1553,14 +1533,14 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
borderRadius: borderRadius.lg,
|
borderRadius: borderRadius.lg,
|
||||||
borderWidth: 1.5,
|
borderWidth: 1,
|
||||||
borderColor: colors.divider + '80',
|
borderColor: colors.divider + '80',
|
||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.default,
|
||||||
},
|
},
|
||||||
joinTypeItemSelected: {
|
joinTypeItemSelected: {
|
||||||
borderColor: colors.primary.main,
|
borderColor: colors.primary.main,
|
||||||
backgroundColor: colors.primary.light + '12',
|
backgroundColor: colors.primary.light + '12',
|
||||||
borderWidth: 2,
|
borderWidth: 1.5,
|
||||||
},
|
},
|
||||||
joinTypeIcon: {
|
joinTypeIcon: {
|
||||||
width: 48,
|
width: 48,
|
||||||
@@ -1602,7 +1582,7 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
transferItemSelected: {
|
transferItemSelected: {
|
||||||
backgroundColor: colors.primary.light + '12',
|
backgroundColor: colors.primary.light + '12',
|
||||||
borderWidth: 1.5,
|
borderWidth: 1,
|
||||||
borderColor: colors.primary.light + '60',
|
borderColor: colors.primary.light + '60',
|
||||||
},
|
},
|
||||||
transferItemInfo: {
|
transferItemInfo: {
|
||||||
@@ -1681,7 +1661,7 @@ function createGroupInfoStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
friendItemSelected: {
|
friendItemSelected: {
|
||||||
backgroundColor: colors.primary.light + '12',
|
backgroundColor: colors.primary.light + '12',
|
||||||
borderWidth: 1.5,
|
borderWidth: 1,
|
||||||
borderColor: colors.primary.light + '60',
|
borderColor: colors.primary.light + '60',
|
||||||
},
|
},
|
||||||
friendInfo: {
|
friendInfo: {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
|
import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
|
||||||
import { useAuthStore } from '../../stores';
|
import { useAuthStore } from '../../stores';
|
||||||
import { authService } from '@/services/auth';
|
import { authService } from '@/services/auth';
|
||||||
import { messageService } from '@/services/message';
|
import { messageService } from '@/services/message';
|
||||||
@@ -336,7 +336,7 @@ const PrivateChatInfoScreen: React.FC = () => {
|
|||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
{/* 用户基本信息卡片 */}
|
{/* 用户基本信息 */}
|
||||||
<View style={styles.headerCard}>
|
<View style={styles.headerCard}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.userHeader}
|
style={styles.userHeader}
|
||||||
@@ -345,7 +345,7 @@ const PrivateChatInfoScreen: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
source={displayUser.avatar}
|
source={displayUser.avatar}
|
||||||
size={90}
|
size={80}
|
||||||
name={displayUser.nickname || ''}
|
name={displayUser.nickname || ''}
|
||||||
/>
|
/>
|
||||||
<View style={styles.userInfo}>
|
<View style={styles.userInfo}>
|
||||||
@@ -421,20 +421,20 @@ function createPrivateChatInfoStyles(colors: AppColors) {
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
pageHeader: {
|
pageHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.lg,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.md,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 0,
|
||||||
borderBottomColor: colors.divider,
|
|
||||||
},
|
},
|
||||||
pageTitle: {
|
pageTitle: {
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
|
fontSize: fontSizes.xl,
|
||||||
},
|
},
|
||||||
pageHeaderPlaceholder: {
|
pageHeaderPlaceholder: {
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -444,15 +444,13 @@ function createPrivateChatInfoStyles(colors: AppColors) {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
paddingBottom: spacing.xl,
|
paddingBottom: spacing.xl,
|
||||||
},
|
},
|
||||||
headerCard: {
|
headerCard: {
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
marginHorizontal: spacing.md,
|
paddingVertical: spacing.lg,
|
||||||
marginTop: spacing.md,
|
marginBottom: spacing.md,
|
||||||
borderRadius: borderRadius.lg,
|
|
||||||
padding: spacing.lg,
|
|
||||||
...shadows.sm,
|
|
||||||
},
|
},
|
||||||
userHeader: {
|
userHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -460,58 +458,61 @@ function createPrivateChatInfoStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
userInfo: {
|
userInfo: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginLeft: spacing.md,
|
marginLeft: spacing.lg,
|
||||||
},
|
},
|
||||||
userName: {
|
userName: {
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
|
fontSize: fontSizes['2xl'],
|
||||||
marginBottom: spacing.xs,
|
marginBottom: spacing.xs,
|
||||||
},
|
},
|
||||||
section: {
|
section: {
|
||||||
marginTop: spacing.lg,
|
marginTop: spacing.lg,
|
||||||
paddingHorizontal: spacing.md,
|
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
marginBottom: spacing.sm,
|
marginBottom: spacing.sm,
|
||||||
marginLeft: spacing.sm,
|
marginLeft: spacing.sm,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
|
fontSize: fontSizes.sm,
|
||||||
|
color: colors.text.secondary,
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
borderRadius: borderRadius.lg,
|
|
||||||
...shadows.sm,
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
settingItem: {
|
settingItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.md,
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: 0,
|
||||||
},
|
},
|
||||||
settingIconContainer: {
|
settingIconContainer: {
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: borderRadius.md,
|
borderRadius: borderRadius.md,
|
||||||
backgroundColor: colors.primary.light + '20',
|
backgroundColor: colors.background.default,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: spacing.md,
|
marginRight: spacing.md,
|
||||||
},
|
},
|
||||||
settingIconDanger: {
|
settingIconDanger: {
|
||||||
backgroundColor: colors.error.light + '20',
|
backgroundColor: colors.error.light + '15',
|
||||||
},
|
},
|
||||||
settingTitle: {
|
settingTitle: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
dangerText: {
|
dangerText: {
|
||||||
color: colors.error.main,
|
color: colors.error.main,
|
||||||
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
divider: {
|
divider: {
|
||||||
marginLeft: spacing.xl + 36,
|
marginLeft: 52,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
marginVertical: spacing.xs,
|
marginVertical: 0,
|
||||||
},
|
},
|
||||||
deleteButton: {
|
deleteButton: {
|
||||||
marginTop: spacing.sm,
|
marginTop: spacing.sm,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
height: 52,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, FlatList, ListRenderItem, Platform } from 'react-native';
|
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, Platform } from 'react-native';
|
||||||
|
import { FlashList, ListRenderItem } from '@shopify/flash-list';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import { Image as ExpoImage } from 'expo-image';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
@@ -72,8 +73,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
...baseStyles,
|
...baseStyles,
|
||||||
emojiContainer: {
|
emojiContainer: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
flexWrap: 'wrap',
|
|
||||||
padding: spacing.sm,
|
padding: spacing.sm,
|
||||||
},
|
},
|
||||||
emojiItem: {
|
emojiItem: {
|
||||||
@@ -160,26 +160,19 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
), [styles.emojiItem, styles.emojiText, handleEmojiPress]);
|
), [styles.emojiItem, styles.emojiText, handleEmojiPress]);
|
||||||
|
|
||||||
// 渲染 Emoji 面板(使用 FlatList 虚拟化,只渲染可见行)
|
// 渲染 Emoji 面板(使用 FlashList 虚拟化,只渲染可见行)
|
||||||
const renderEmojiPanel = () => (
|
const renderEmojiPanel = () => (
|
||||||
<FlatList
|
<View style={styles.emojiContainer}>
|
||||||
key="emoji-flatlist"
|
<FlashList
|
||||||
data={emojiRows}
|
key="emoji-flatlist"
|
||||||
keyExtractor={(item) => item.id}
|
data={emojiRows}
|
||||||
renderItem={renderEmojiRow}
|
keyExtractor={(item) => item.id}
|
||||||
contentContainerStyle={styles.emojiContainer}
|
renderItem={renderEmojiRow}
|
||||||
showsVerticalScrollIndicator={true}
|
showsVerticalScrollIndicator={true}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
initialNumToRender={12}
|
style={{ flex: 1 }}
|
||||||
maxToRenderPerBatch={12}
|
/>
|
||||||
windowSize={5}
|
</View>
|
||||||
removeClippedSubviews={true}
|
|
||||||
getItemLayout={(_data, index) => ({
|
|
||||||
length: 60,
|
|
||||||
offset: 60 * index,
|
|
||||||
index,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 添加表情(从相册选择)
|
// 添加表情(从相册选择)
|
||||||
@@ -347,20 +340,18 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<View style={styles.stickerGrid}>
|
||||||
key="sticker-flatlist"
|
<FlashList
|
||||||
data={stickerListData}
|
key="sticker-flatlist"
|
||||||
numColumns={4}
|
data={stickerListData}
|
||||||
keyExtractor={(item) => item.type === 'manage' ? 'manage-button' : item.sticker.id}
|
numColumns={4}
|
||||||
renderItem={renderStickerListItem}
|
keyExtractor={(item) => item.type === 'manage' ? 'manage-button' : item.sticker.id}
|
||||||
contentContainerStyle={styles.stickerGrid}
|
renderItem={renderStickerListItem}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
initialNumToRender={16}
|
style={{ flex: 1 }}
|
||||||
maxToRenderPerBatch={16}
|
/>
|
||||||
windowSize={7}
|
</View>
|
||||||
removeClippedSubviews={true}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -494,19 +485,17 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
|||||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<View style={styles.manageStickerGrid}>
|
||||||
data={manageListData}
|
<FlashList
|
||||||
numColumns={4}
|
data={manageListData}
|
||||||
keyExtractor={(item) => item.type === 'add' ? 'manage-add-button' : item.sticker.id}
|
numColumns={4}
|
||||||
renderItem={renderManageListItem}
|
keyExtractor={(item) => item.type === 'add' ? 'manage-add-button' : item.sticker.id}
|
||||||
contentContainerStyle={styles.manageStickerGrid}
|
renderItem={renderManageListItem}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
initialNumToRender={16}
|
style={{ flex: 1 }}
|
||||||
maxToRenderPerBatch={16}
|
/>
|
||||||
windowSize={7}
|
</View>
|
||||||
removeClippedSubviews={true}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 底部操作栏(管理模式下显示) */}
|
{/* 底部操作栏(管理模式下显示) */}
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
|
|||||||
<View style={[styles.messageContent, isMe ? styles.myMessageContentPanel : null]}>
|
<View style={[styles.messageContent, isMe ? styles.myMessageContentPanel : null]}>
|
||||||
{/* 群聊模式:显示发送者昵称 */}
|
{/* 群聊模式:显示发送者昵称 */}
|
||||||
{isGroupChat && senderInfo && (
|
{isGroupChat && senderInfo && (
|
||||||
<Text style={[styles.senderName, isMe && styles.mySenderName]}>{senderInfo.nickname}</Text>
|
<Text style={[styles.senderName, isMe ? styles.mySenderName : {}]}>{senderInfo.nickname}</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{renderMessageContent()}
|
{renderMessageContent()}
|
||||||
|
|||||||
@@ -711,8 +711,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
|||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
},
|
},
|
||||||
stickerGrid: {
|
stickerGrid: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
flexWrap: 'wrap',
|
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingTop: spacing.md,
|
paddingTop: spacing.md,
|
||||||
paddingBottom: spacing.xl,
|
paddingBottom: spacing.xl,
|
||||||
@@ -1259,8 +1258,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
|||||||
color: colors.chat.textSecondary,
|
color: colors.chat.textSecondary,
|
||||||
},
|
},
|
||||||
manageStickerGrid: {
|
manageStickerGrid: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
flexWrap: 'wrap',
|
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingTop: spacing.md,
|
paddingTop: spacing.md,
|
||||||
paddingBottom: spacing.xl,
|
paddingBottom: spacing.xl,
|
||||||
|
|||||||
@@ -562,8 +562,17 @@ export const useChatScreen = (props?: ChatScreenProps) => {
|
|||||||
const jumpToLatestMessages = useCallback(() => {
|
const jumpToLatestMessages = useCallback(() => {
|
||||||
suppressAutoFollowRef.current = false;
|
suppressAutoFollowRef.current = false;
|
||||||
isBrowsingHistoryRef.current = false;
|
isBrowsingHistoryRef.current = false;
|
||||||
scrollToLatest(true, true, 'jump-latest-button');
|
// FlashList 在 inverted 模式下 scrollToOffset({ offset: 0 }) 可能无法真正滚到最底部,
|
||||||
}, [scrollToLatest]);
|
// 因为列表内容高度变化后最小 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]);
|
||||||
|
|
||||||
const setBrowsingHistory = useCallback((browsing: boolean) => {
|
const setBrowsingHistory = useCallback((browsing: boolean) => {
|
||||||
isBrowsingHistoryRef.current = browsing;
|
isBrowsingHistoryRef.current = browsing;
|
||||||
|
|||||||
@@ -110,35 +110,48 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
|
|||||||
);
|
);
|
||||||
}, [loading, activeTab, mode, isBlockedProfile]);
|
}, [loading, activeTab, mode, isBlockedProfile]);
|
||||||
|
|
||||||
|
// 渲染用户信息头部(与 TabBar 分离,避免切换 tab 时重渲染)
|
||||||
|
const renderUserHeader = useMemo(() => {
|
||||||
|
if (!user) return null;
|
||||||
|
return (
|
||||||
|
<UserProfileHeader
|
||||||
|
user={user}
|
||||||
|
isCurrentUser={isCurrentUser}
|
||||||
|
isBlocked={isBlocked}
|
||||||
|
onFollow={handleFollow}
|
||||||
|
onSettings={handleSettings}
|
||||||
|
onEditProfile={handleEditProfile}
|
||||||
|
onMessage={handleMessage}
|
||||||
|
onBlock={handleBlock}
|
||||||
|
onFollowingPress={handleFollowingPress}
|
||||||
|
onFollowersPress={handleFollowersPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress]);
|
||||||
|
|
||||||
|
// 渲染 TabBar
|
||||||
|
const renderTabBar = useMemo(() => (
|
||||||
|
<View style={sharedStyles.tabBarContainer}>
|
||||||
|
<TabBar
|
||||||
|
tabs={TABS}
|
||||||
|
activeIndex={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
variant="modern"
|
||||||
|
icons={TAB_ICONS}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
), [activeTab, setActiveTab, sharedStyles.tabBarContainer]);
|
||||||
|
|
||||||
// 渲染 FlashList 头部(用户信息 + TabBar)
|
// 渲染 FlashList 头部(用户信息 + TabBar)
|
||||||
const renderListHeader = useCallback(() => {
|
const renderListHeader = useCallback(() => {
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserProfileHeader
|
{renderUserHeader}
|
||||||
user={user}
|
{renderTabBar}
|
||||||
isCurrentUser={isCurrentUser}
|
|
||||||
isBlocked={isBlocked}
|
|
||||||
onFollow={handleFollow}
|
|
||||||
onSettings={handleSettings}
|
|
||||||
onEditProfile={handleEditProfile}
|
|
||||||
onMessage={handleMessage}
|
|
||||||
onBlock={handleBlock}
|
|
||||||
onFollowingPress={handleFollowingPress}
|
|
||||||
onFollowersPress={handleFollowersPress}
|
|
||||||
/>
|
|
||||||
<View style={sharedStyles.tabBarContainer}>
|
|
||||||
<TabBar
|
|
||||||
tabs={TABS}
|
|
||||||
activeIndex={activeTab}
|
|
||||||
onTabChange={setActiveTab}
|
|
||||||
variant="modern"
|
|
||||||
icons={TAB_ICONS}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress, activeTab, setActiveTab]);
|
}, [user, renderUserHeader, renderTabBar]);
|
||||||
|
|
||||||
// 未登录/用户不存在状态
|
// 未登录/用户不存在状态
|
||||||
if (mode === 'self' && !currentUser) {
|
if (mode === 'self' && !currentUser) {
|
||||||
|
|||||||
@@ -251,12 +251,15 @@ class PostService {
|
|||||||
page_size: params.page_size,
|
page_size: params.page_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将 post_type 映射为后端的 tab 参数
|
if (params.post_type) {
|
||||||
if (params.post_type) {
|
requestParams.tab = params.post_type;
|
||||||
requestParams.tab = params.post_type;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.get<any>('/posts', requestParams);
|
if (params.channel_id) {
|
||||||
|
requestParams.channel_id = params.channel_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get<any>('/posts', requestParams);
|
||||||
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export class NetworkCursorPostListPagedSource implements IPostListPagedSource {
|
|||||||
cursor: this.nextCursor ?? '',
|
cursor: this.nextCursor ?? '',
|
||||||
page_size: this.pageSize,
|
page_size: this.pageSize,
|
||||||
post_type: this.tab,
|
post_type: this.tab,
|
||||||
|
channel_id: this.channelId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await postService.getPostsCursor(params);
|
const result = await postService.getPostsCursor(params);
|
||||||
|
|||||||
@@ -520,14 +520,11 @@ export type DeviceType = 'ios' | 'android' | 'web';
|
|||||||
* 游标分页请求参数
|
* 游标分页请求参数
|
||||||
*/
|
*/
|
||||||
export interface CursorPaginationRequest {
|
export interface CursorPaginationRequest {
|
||||||
/** 游标字符串(可选,首次请求不传) */
|
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
/** 分页方向:forward 或 backward(默认 forward) */
|
|
||||||
direction?: 'forward' | 'backward';
|
direction?: 'forward' | 'backward';
|
||||||
/** 每页数量(默认 20,最大 100) */
|
|
||||||
page_size?: number;
|
page_size?: number;
|
||||||
/** 帖子类型筛选(可选):follow, hot, latest */
|
|
||||||
post_type?: 'follow' | 'hot' | 'latest';
|
post_type?: 'follow' | 'hot' | 'latest';
|
||||||
|
channel_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user