feat(ui): unify design system to Twitter/X style across screens and improve message handling
Redesign multiple screens and components from card-based responsive design to Twitter/X flat design: - UserProfileHeader: adopt Twitter/X style with larger icons, simplified buttons, and block functionality - AppBackButton: update to chevron-left icon with larger size, remove background styling - Update SettingsScreen, EditProfileScreen, and UserProfileScreen with flat layout approach - ConversationListRow: convert from bordered cards to flat list rows with updated typography Improve message system with WebSocket notification handling: - Add real-time system unread count updates via WebSocket - Track lastSystemMessageAt for accurate system message timestamps - Add notification deduplication and read-status filtering - Combine system and conversation unread counts in background sync - Clear system notifications from notification center on mark as read
This commit is contained in:
@@ -1108,10 +1108,10 @@ function createGroupInfoStyles(colors: AppColors) {
|
||||
fontSize: fontSizes.xl,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
pageHeaderPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
pageHeaderPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -690,6 +690,7 @@ function createGroupMembersStyles(colors: AppColors) {
|
||||
},
|
||||
pageHeaderPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
section: {
|
||||
marginBottom: spacing.md,
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
useUnreadCount,
|
||||
useMarkAsRead,
|
||||
useMessageManagerConversations,
|
||||
useMessageStore,
|
||||
} from '../../stores';
|
||||
import { Avatar, Text, EmptyState, ResponsiveContainer, AppBackButton } from '../../components/common';
|
||||
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
|
||||
@@ -113,6 +114,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
|
||||
// 使用 MessageManager 获取未读数和系统通知数
|
||||
const { totalUnreadCount, systemUnreadCount } = useUnreadCount();
|
||||
const lastSystemMessageAt = useMessageStore(state => state.lastSystemMessageAt);
|
||||
const { markAllAsRead, isMarking } = useMarkAsRead(null);
|
||||
|
||||
// 【新架构】使用MessageManager的hook创建会话
|
||||
@@ -160,14 +162,14 @@ export const MessageListScreen: React.FC = () => {
|
||||
seq: 0,
|
||||
segments: [{ type: 'text', data: { text: '系统通知' } }],
|
||||
status: 'normal',
|
||||
created_at: new Date().toISOString(),
|
||||
created_at: lastSystemMessageAt || new Date().toISOString(),
|
||||
},
|
||||
last_message_at: new Date().toISOString(),
|
||||
last_message_at: lastSystemMessageAt || new Date().toISOString(),
|
||||
unread_count: systemUnreadCount,
|
||||
participants: [],
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}), [systemUnreadCount]);
|
||||
created_at: lastSystemMessageAt || new Date().toISOString(),
|
||||
updated_at: lastSystemMessageAt || new Date().toISOString(),
|
||||
}), [systemUnreadCount, lastSystemMessageAt]);
|
||||
|
||||
// 动画值
|
||||
const [scaleAnims] = useState(() =>
|
||||
@@ -451,6 +453,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
// 创建系统通知会话项
|
||||
const createSystemMessageItem = (): ConversationResponse => {
|
||||
const noticeContent = systemUnreadCount > 0 ? `您有 ${systemUnreadCount} 条未读通知` : '暂无新通知';
|
||||
const lastTime = lastSystemMessageAt || new Date().toISOString();
|
||||
return {
|
||||
id: SYSTEM_MESSAGE_CHANNEL_ID,
|
||||
type: 'private',
|
||||
@@ -462,13 +465,13 @@ export const MessageListScreen: React.FC = () => {
|
||||
seq: 0,
|
||||
segments: [{ type: 'text', data: { text: noticeContent } }],
|
||||
status: 'normal',
|
||||
created_at: new Date().toISOString(),
|
||||
created_at: lastTime,
|
||||
},
|
||||
last_message_at: new Date().toISOString(),
|
||||
last_message_at: lastTime,
|
||||
unread_count: systemUnreadCount,
|
||||
participants: [],
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
created_at: lastTime,
|
||||
updated_at: lastTime,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -936,7 +939,8 @@ function createMessageListStyles(colors: AppColors) {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.default,
|
||||
...shadows.sm,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.divider + '60',
|
||||
},
|
||||
headerWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
@@ -949,7 +953,7 @@ function createMessageListStyles(colors: AppColors) {
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingHorizontal: spacing.xs,
|
||||
},
|
||||
headerLeft: {
|
||||
width: 44,
|
||||
@@ -1028,6 +1032,7 @@ function createMessageListStyles(colors: AppColors) {
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
paddingHorizontal: spacing.xs,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
@@ -1086,11 +1091,9 @@ function createMessageListStyles(colors: AppColors) {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
marginBottom: spacing.sm,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: `${colors.divider}50`,
|
||||
backgroundColor: colors.background.default,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: `${colors.divider}50`,
|
||||
},
|
||||
searchResultContent: {
|
||||
flex: 1,
|
||||
|
||||
@@ -32,9 +32,10 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { spacing, borderRadius, useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
|
||||
import { SystemMessageResponse } from '../../types/dto';
|
||||
import { messageService } from '@/services/message';
|
||||
import { systemNotificationService } from '@/services/notification';
|
||||
import { commentService } from '@/services/post';
|
||||
import { SystemMessageItem } from '../../components/business';
|
||||
import { Text, ResponsiveContainer } from '../../components/common';
|
||||
import { Text, ResponsiveContainer, AppBackButton } from '../../components/common';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { useMessageManagerSystemUnreadCount } from '../../stores';
|
||||
@@ -174,6 +175,8 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
setSystemUnreadCount(0);
|
||||
// 同步更新全局 TabBar 红点
|
||||
messageManager.fetchUnreadCount();
|
||||
// 清除通知栏中已读的系统消息通知
|
||||
systemNotificationService.clearAllNotifications().catch(() => {});
|
||||
} catch (error) {
|
||||
console.error('一键已读失败:', error);
|
||||
}
|
||||
@@ -381,11 +384,9 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
return (
|
||||
<View style={[styles.header, isWideScreen ? styles.headerWide : null]}>
|
||||
{shouldShowBackButton ? (
|
||||
<TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.8}>
|
||||
<View style={styles.backIconButton}>
|
||||
<MaterialCommunityIcons name="arrow-left" size={22} color={colors.text.primary} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.backButton}>
|
||||
<AppBackButton onPress={onBack} />
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.backButtonPlaceholder} />
|
||||
)}
|
||||
@@ -673,14 +674,6 @@ function createNotificationsStyles(colors: AppColors) {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backIconButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.background.default,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
backButtonPlaceholder: {
|
||||
width: 40,
|
||||
},
|
||||
@@ -735,7 +728,7 @@ function createNotificationsStyles(colors: AppColors) {
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 24,
|
||||
paddingBottom: 88,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: 32,
|
||||
|
||||
@@ -106,12 +106,12 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
|
||||
height: 56,
|
||||
},
|
||||
backButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'transparent',
|
||||
marginLeft: -12,
|
||||
padding: 0,
|
||||
},
|
||||
headerCenter: {
|
||||
flex: 1,
|
||||
|
||||
@@ -157,32 +157,27 @@ function createConversationRowStyles(colors: AppColors) {
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: colors.background.paper,
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.sm,
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.divider + '40',
|
||||
backgroundColor: colors.background.default,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.chat.borderLight,
|
||||
},
|
||||
conversationItemSelected: {
|
||||
backgroundColor: colors.primary.light + '12',
|
||||
borderColor: colors.primary.main + '60',
|
||||
borderWidth: 1.5,
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
},
|
||||
avatarContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
systemAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: borderRadius.md,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
conversationContent: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
marginLeft: spacing.lg,
|
||||
},
|
||||
conversationHeader: {
|
||||
flexDirection: 'row',
|
||||
@@ -197,49 +192,49 @@ function createConversationRowStyles(colors: AppColors) {
|
||||
officialBadge: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 1,
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
officialBadgeText: {
|
||||
color: colors.primary.contrast,
|
||||
fontSize: 10,
|
||||
fontSize: 9,
|
||||
fontWeight: '800',
|
||||
},
|
||||
userName: {
|
||||
fontWeight: '700',
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.3,
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
groupIcon: {
|
||||
marginRight: 4,
|
||||
marginRight: 3,
|
||||
},
|
||||
memberCount: {
|
||||
fontSize: 12,
|
||||
fontSize: 13,
|
||||
color: colors.text.secondary,
|
||||
marginLeft: 2,
|
||||
fontWeight: '500',
|
||||
},
|
||||
pinnedIcon: {
|
||||
marginLeft: 6,
|
||||
marginLeft: 5,
|
||||
},
|
||||
groupAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
width: 56,
|
||||
height: 56,
|
||||
},
|
||||
groupAvatarPlaceholder: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
timeText: {
|
||||
color: colors.text.secondary,
|
||||
color: colors.text.hint,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
fontWeight: '400',
|
||||
},
|
||||
messageRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -250,25 +245,26 @@ function createConversationRowStyles(colors: AppColors) {
|
||||
color: colors.text.secondary,
|
||||
fontSize: 14,
|
||||
fontWeight: '400',
|
||||
lineHeight: 20,
|
||||
},
|
||||
unreadMessageText: {
|
||||
color: colors.text.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
unreadBadge: {
|
||||
minWidth: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
backgroundColor: colors.primary.main,
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.error.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
paddingHorizontal: 6,
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
unreadBadgeText: {
|
||||
color: colors.primary.contrast,
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -321,12 +317,12 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
<View style={styles.avatarContainer}>
|
||||
{isSystemChannel ? (
|
||||
<View style={styles.systemAvatar}>
|
||||
<MaterialCommunityIcons name="bell-ring" size={24} color={colors.primary.contrast} />
|
||||
<MaterialCommunityIcons name="bell-ring" size={26} color={colors.primary.contrast} />
|
||||
</View>
|
||||
) : isGroupChat ? (
|
||||
<View style={styles.groupAvatar}>
|
||||
{displayAvatar ? (
|
||||
<Avatar source={displayAvatar} size={50} name={displayName} />
|
||||
<Avatar source={displayAvatar} size={56} name={displayName} />
|
||||
) : (
|
||||
<View style={styles.groupAvatarPlaceholder}>
|
||||
<MaterialCommunityIcons name="account-group" size={28} color={colors.primary.contrast} />
|
||||
@@ -334,7 +330,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Avatar source={displayAvatar} size={50} name={displayName} />
|
||||
<Avatar source={displayAvatar} size={56} name={displayName} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user