refactor(PostCard, PostCardGrid, HomeScreen): enhance PostCard component and remove PostCardGrid
- Introduced a new PostCardRelativeTime component for dynamic time display, improving user experience with real-time updates. - Refactored PostCard to utilize memoization for performance optimization and prevent unnecessary re-renders. - Removed the PostCardGrid component to streamline the codebase, consolidating functionality within the PostCard component. - Updated HomeScreen to leverage the new PostCard features, ensuring consistent post rendering and interaction handling.
This commit is contained in:
@@ -31,7 +31,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, fontSizes, shadows, borderRadius } from '../../theme';
|
||||
import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments, extractTextFromSegmentsAsync, MessageSegment } from '../../types/dto';
|
||||
import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments } from '../../types/dto';
|
||||
import { authService } from '../../services';
|
||||
import { useUserStore, useAuthStore } from '../../stores';
|
||||
// 【新架构】使用MessageManager hooks(会话列表数据源自 MessageManager 游标同步)
|
||||
@@ -46,9 +46,9 @@ import {
|
||||
import { Avatar, Text, EmptyState, ResponsiveContainer } from '../../components/common';
|
||||
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
|
||||
import { RootStackParamList, MessageStackParamList } from '../../navigation/types';
|
||||
import { getUserCache } from '../../services/database';
|
||||
// 导入 EmbeddedChat 组件用于桌面端双栏布局
|
||||
import { EmbeddedChat } from './components/EmbeddedChat';
|
||||
import { ConversationListRow } from './components/ConversationListRow';
|
||||
// 导入 NotificationsScreen 用于在内部显示
|
||||
import { NotificationsScreen } from './NotificationsScreen';
|
||||
// 导入扫码组件
|
||||
@@ -70,80 +70,6 @@ interface SearchResultItem {
|
||||
matchedMessages?: MessageResponse[];
|
||||
}
|
||||
|
||||
// 动画值
|
||||
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
|
||||
const MAX_CONVERSATION_NAME_LENGTH = 10;
|
||||
|
||||
const truncateDisplayName = (name: string, maxLength: number = MAX_CONVERSATION_NAME_LENGTH): string => {
|
||||
if (!name) return '';
|
||||
if (name.length <= maxLength) return name;
|
||||
return `${name.slice(0, maxLength)}...`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 异步消息预览组件
|
||||
*/
|
||||
const AsyncMessagePreview: React.FC<{
|
||||
segments?: MessageSegment[];
|
||||
status?: string;
|
||||
isGroupChat?: boolean;
|
||||
senderName?: string;
|
||||
}> = ({ segments, status, isGroupChat, senderName }) => {
|
||||
const [displayText, setDisplayText] = useState<string>('');
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
const loadPreview = async () => {
|
||||
if (status === 'recalled') {
|
||||
if (isMountedRef.current) {
|
||||
setDisplayText('消息已撤回');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const initialText = extractTextFromSegments(segments);
|
||||
if (isMountedRef.current) {
|
||||
setDisplayText(initialText);
|
||||
}
|
||||
|
||||
const hasAtWithoutNickname = segments?.some(
|
||||
s => s.type === 'at' && s.data.user_id !== 'all' && !s.data.nickname
|
||||
);
|
||||
|
||||
if (hasAtWithoutNickname) {
|
||||
try {
|
||||
const asyncText = await extractTextFromSegmentsAsync(
|
||||
segments,
|
||||
getUserCache,
|
||||
authService.getUserById.bind(authService)
|
||||
);
|
||||
if (isMountedRef.current && asyncText !== initialText) {
|
||||
setDisplayText(asyncText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[AsyncMessagePreview] 获取用户名失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadPreview();
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, [segments, status]);
|
||||
|
||||
if (!displayText) return null;
|
||||
|
||||
if (isGroupChat && senderName) {
|
||||
return <>{senderName}: {displayText}</>;
|
||||
}
|
||||
|
||||
return <>{displayText}</>;
|
||||
};
|
||||
|
||||
/**
|
||||
* MessageListScreen - 纯渲染组件
|
||||
* 所有数据通过useMessageList hook获取
|
||||
@@ -301,8 +227,8 @@ export const MessageListScreen: React.FC = () => {
|
||||
setRefreshing(false);
|
||||
}, [refresh]);
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string): string => {
|
||||
// 格式化时间(稳定引用,配合会话行 memo)
|
||||
const formatTime = useCallback((dateString: string): string => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
@@ -327,7 +253,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 跳转到聊天页
|
||||
const handleConversationPress = (conversation: ConversationResponse, index: number) => {
|
||||
@@ -380,6 +306,9 @@ export const MessageListScreen: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleConversationPressRef = useRef(handleConversationPress);
|
||||
handleConversationPressRef.current = handleConversationPress;
|
||||
|
||||
// 创建群聊
|
||||
const handleCreateGroup = () => {
|
||||
(navigation as any).navigate('CreateGroup');
|
||||
@@ -533,136 +462,37 @@ export const MessageListScreen: React.FC = () => {
|
||||
...conversationList,
|
||||
];
|
||||
|
||||
const listConvRef = useRef(new Map<string, ConversationResponse>());
|
||||
listConvRef.current = new Map(listData.map((c) => [String(c.id), c]));
|
||||
|
||||
const stableConversationPress = useCallback((conversationId: string, index: number) => {
|
||||
const c = listConvRef.current.get(conversationId);
|
||||
if (c) {
|
||||
handleConversationPressRef.current(c, index);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 总未读数
|
||||
const totalUnread = totalUnreadCount + systemUnreadCount;
|
||||
|
||||
// 渲染会话项
|
||||
const renderConversation = ({ item, index }: { item: ConversationResponse; index: number }) => {
|
||||
const isSystemChannel = item.id === SYSTEM_MESSAGE_CHANNEL_ID;
|
||||
const isGroupChat = !!(item.type === 'group' && item.group);
|
||||
|
||||
// 获取显示名称和头像
|
||||
let displayNameRaw: string;
|
||||
let displayName: string;
|
||||
let displayAvatar: string | null = null;
|
||||
|
||||
if (isSystemChannel) {
|
||||
displayNameRaw = '系统通知';
|
||||
} else if (isGroupChat && item.group) {
|
||||
displayNameRaw = item.group.name || '群聊';
|
||||
displayAvatar = item.group.avatar && item.group.avatar.trim() !== '' ? item.group.avatar : null;
|
||||
} else {
|
||||
displayNameRaw = item.participants?.[0]?.nickname || '未知用户';
|
||||
displayAvatar = item.participants?.[0]?.avatar || null;
|
||||
}
|
||||
displayName = truncateDisplayName(displayNameRaw);
|
||||
|
||||
const getSenderName = (): string | undefined => {
|
||||
if (isGroupChat && item.last_message?.sender) {
|
||||
return item.last_message.sender.nickname || item.last_message.sender.username;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 是否选中(用于桌面端双栏布局高亮)
|
||||
const isSelected = selectedConversation?.id === item.id;
|
||||
|
||||
return (
|
||||
<AnimatedTouchable
|
||||
style={[
|
||||
styles.conversationItem,
|
||||
isSelected && styles.conversationItemSelected,
|
||||
{ transform: [{ scale: scaleAnims[index] || 1 }] }
|
||||
]}
|
||||
onPress={() => handleConversationPress(item, index)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.avatarContainer}>
|
||||
{isSystemChannel ? (
|
||||
<View style={styles.systemAvatar}>
|
||||
<MaterialCommunityIcons name="bell-ring" size={24} color="#FFF" />
|
||||
</View>
|
||||
) : isGroupChat ? (
|
||||
<View style={styles.groupAvatar}>
|
||||
{displayAvatar ? (
|
||||
<Avatar source={displayAvatar} size={50} name={displayName} />
|
||||
) : (
|
||||
<View style={styles.groupAvatarPlaceholder}>
|
||||
<MaterialCommunityIcons name="account-group" size={28} color="#FFF" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Avatar source={displayAvatar} size={50} name={displayName} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.conversationContent}>
|
||||
<View style={styles.conversationHeader}>
|
||||
<View style={styles.nameRow}>
|
||||
{isSystemChannel && (
|
||||
<View style={styles.officialBadge}>
|
||||
<Text style={styles.officialBadgeText}>官方</Text>
|
||||
</View>
|
||||
)}
|
||||
{isGroupChat && (
|
||||
<MaterialCommunityIcons
|
||||
name="account-group"
|
||||
size={16}
|
||||
color={colors.primary.main}
|
||||
style={styles.groupIcon}
|
||||
/>
|
||||
)}
|
||||
<Text style={styles.userName}>{displayName}</Text>
|
||||
{item.is_pinned && !isSystemChannel && (
|
||||
<MaterialCommunityIcons
|
||||
name="pin"
|
||||
size={14}
|
||||
color={colors.warning.main}
|
||||
style={styles.pinnedIcon}
|
||||
/>
|
||||
)}
|
||||
{isGroupChat && item.member_count !== undefined && (
|
||||
<Text style={styles.memberCount}>({item.member_count})</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.timeText}>
|
||||
{formatTime(item.last_message_at || item.updated_at)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.messageRow}>
|
||||
<Text
|
||||
style={item.unread_count > 0 ? [styles.messageText, styles.unreadMessageText] : styles.messageText}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{!item.last_message ? (
|
||||
'暂无消息'
|
||||
) : (
|
||||
<AsyncMessagePreview
|
||||
segments={item.last_message?.segments}
|
||||
status={item.last_message?.status}
|
||||
isGroupChat={isGroupChat}
|
||||
senderName={getSenderName()}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
{item.unread_count > 0 && (
|
||||
<View style={styles.unreadBadge}>
|
||||
<Text style={styles.unreadBadgeText}>
|
||||
{item.unread_count > 99 ? '99+' : item.unread_count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{__DEV__ && (
|
||||
<Text style={{fontSize: 10, color: '#999', marginLeft: 8}}>
|
||||
[DEBUG: {item.unread_count || 0}]
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</AnimatedTouchable>
|
||||
);
|
||||
};
|
||||
// 渲染会话项(行级 memo + 按 id 解析最新会话,避免 Tab 切换时整表无效重绘)
|
||||
const renderConversation = useCallback(
|
||||
({ item, index }: { item: ConversationResponse; index: number }) => {
|
||||
const isSelected = selectedConversation?.id === item.id;
|
||||
return (
|
||||
<ConversationListRow
|
||||
item={item}
|
||||
index={index}
|
||||
scale={scaleAnims[index] ?? 1}
|
||||
isSelected={isSelected}
|
||||
formatTime={formatTime}
|
||||
systemChannelId={SYSTEM_MESSAGE_CHANNEL_ID}
|
||||
onPress={() => stableConversationPress(String(item.id), index)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[selectedConversation?.id, scaleAnims, formatTime, stableConversationPress]
|
||||
);
|
||||
|
||||
// 渲染空状态
|
||||
const renderEmpty = () => (
|
||||
@@ -1187,119 +1017,6 @@ const styles = StyleSheet.create({
|
||||
flexGrow: 1,
|
||||
backgroundColor: '#FAFAFA',
|
||||
},
|
||||
conversationItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: '#FFF',
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.sm,
|
||||
borderRadius: 12,
|
||||
...shadows.sm,
|
||||
},
|
||||
conversationItemSelected: {
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
borderColor: colors.primary.main,
|
||||
borderWidth: 1,
|
||||
},
|
||||
avatarContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
systemAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
conversationContent: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
conversationHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
nameRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
officialBadge: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
officialBadgeText: {
|
||||
color: '#FFF',
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
userName: {
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
fontSize: 16,
|
||||
},
|
||||
groupIcon: {
|
||||
marginRight: 4,
|
||||
},
|
||||
memberCount: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
marginLeft: 2,
|
||||
},
|
||||
pinnedIcon: {
|
||||
marginLeft: 6,
|
||||
},
|
||||
groupAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
groupAvatarPlaceholder: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
timeText: {
|
||||
color: '#999',
|
||||
fontSize: 12,
|
||||
},
|
||||
messageRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
messageText: {
|
||||
flex: 1,
|
||||
color: '#888',
|
||||
fontSize: 14,
|
||||
},
|
||||
unreadMessageText: {
|
||||
color: '#333',
|
||||
fontWeight: '500',
|
||||
},
|
||||
unreadBadge: {
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
unreadBadgeText: {
|
||||
color: '#FFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
|
||||
Reference in New Issue
Block a user