From 72842352d9802f8a0c126b23ce15988d5aa26bd6 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Wed, 1 Apr 2026 16:30:44 +0800 Subject: [PATCH] feat(chat): enhance chat settings and message bubble styles - Introduced dynamic chat settings for font size and message bubble radius, improving user customization. - Updated ChatScreen styles to support dynamic themes and responsive layouts. - Refactored bubble styles to utilize dynamic properties for better visual consistency. - Simplified chat settings management by integrating Zustand store for state management. This update significantly enhances the chat interface and user experience by allowing personalized settings and improved visual elements. --- metro.config.js | 4 + src/components/common/ImageGrid.tsx | 4 +- src/components/common/StepIndicator.tsx | 2 +- .../common/VerificationCodeInput.tsx | 6 +- src/screens/auth/RegisterScreen.tsx | 1 - src/screens/auth/RegisterStep1Email.tsx | 6 +- .../components/ChatScreen/bubbleStyles.ts | 106 ++++++++- .../message/components/ChatScreen/styles.ts | 163 +++++++++----- src/screens/profile/ChatSettingsScreen.tsx | 52 +++-- src/stores/authStore.ts | 19 +- src/stores/callStore.ts | 12 +- src/stores/chatSettingsStore.ts | 201 ++++++++++++++++++ src/stores/index.ts | 13 ++ src/stores/sessionStore.ts | 27 +++ 14 files changed, 507 insertions(+), 109 deletions(-) create mode 100644 src/stores/chatSettingsStore.ts create mode 100644 src/stores/sessionStore.ts diff --git a/metro.config.js b/metro.config.js index 08eac42..d54c108 100644 --- a/metro.config.js +++ b/metro.config.js @@ -6,6 +6,10 @@ const config = getDefaultConfig(__dirname); // Add wasm asset support config.resolver.assetExts.push('wasm'); +// Fix for react-native-webrtc event-target-shim import warning +// The package doesn't properly export "./index" in its exports field +config.resolver.unstable_enablePackageExports = false; + // NOTE: enhanceMiddleware is marked as deprecated in Metro's types, // but there's currently no official alternative for custom dev server headers. // For production, configure COEP/COOP headers on your hosting platform. diff --git a/src/components/common/ImageGrid.tsx b/src/components/common/ImageGrid.tsx index bf5678b..402fedc 100644 --- a/src/components/common/ImageGrid.tsx +++ b/src/components/common/ImageGrid.tsx @@ -132,10 +132,10 @@ function createImageGridStyles(colors: AppColors) { aspectRatio: 1, }, gridItem2: { - width: '49%', + width: '48.5%', }, gridItem3: { - width: '32.5%', + width: '31.8%', }, masonryContainer: { flexDirection: 'row', diff --git a/src/components/common/StepIndicator.tsx b/src/components/common/StepIndicator.tsx index bda89fd..83e4fec 100644 --- a/src/components/common/StepIndicator.tsx +++ b/src/components/common/StepIndicator.tsx @@ -12,7 +12,7 @@ export interface Step { title: string; } -interface StepIndicatorProps { +export interface StepIndicatorProps { steps: Step[]; currentStep: number; containerStyle?: ViewStyle; diff --git a/src/components/common/VerificationCodeInput.tsx b/src/components/common/VerificationCodeInput.tsx index 78ef14b..e183029 100644 --- a/src/components/common/VerificationCodeInput.tsx +++ b/src/components/common/VerificationCodeInput.tsx @@ -201,7 +201,7 @@ export const VerificationCodeInput: React.FC = ({ }; function createStyles(colors: AppColors) { - return { + return StyleSheet.create({ container: { alignItems: 'center', justifyContent: 'center', @@ -225,7 +225,7 @@ function createStyles(colors: AppColors) { }, cellFocused: { borderColor: colors.primary.main, - backgroundColor: `${colors.primary.main}10`, // 10% 透明度的主色 + backgroundColor: `${colors.primary.main}10`, }, cellFilled: { borderColor: colors.primary.main, @@ -250,7 +250,7 @@ function createStyles(colors: AppColors) { backgroundColor: colors.primary.main, borderRadius: 1, }, - }; + }); } export default VerificationCodeInput; diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx index f39e883..ff154e7 100644 --- a/src/screens/auth/RegisterScreen.tsx +++ b/src/screens/auth/RegisterScreen.tsx @@ -262,7 +262,6 @@ export const RegisterScreen: React.FC = () => { {/* 步骤指示器 */} diff --git a/src/screens/auth/RegisterStep1Email.tsx b/src/screens/auth/RegisterStep1Email.tsx index b87c211..15f5f66 100644 --- a/src/screens/auth/RegisterStep1Email.tsx +++ b/src/screens/auth/RegisterStep1Email.tsx @@ -105,16 +105,16 @@ export const RegisterStep1Email: React.FC = ({ 0) && styles.actionButtonDisabled, + (sendingCode || (countdown ?? 0) > 0) && styles.actionButtonDisabled, ]} onPress={handleSendCode} - disabled={sendingCode || countdown > 0} + disabled={sendingCode || (countdown ?? 0) > 0} activeOpacity={0.9} > {sendingCode ? '发送中...' - : countdown > 0 + : (countdown ?? 0) > 0 ? `${countdown}秒后重试` : '获取验证码'} diff --git a/src/screens/message/components/ChatScreen/bubbleStyles.ts b/src/screens/message/components/ChatScreen/bubbleStyles.ts index 9f47781..eff1a56 100644 --- a/src/screens/message/components/ChatScreen/bubbleStyles.ts +++ b/src/screens/message/components/ChatScreen/bubbleStyles.ts @@ -5,13 +5,20 @@ import { StyleSheet, ViewStyle, TextStyle } from 'react-native'; import { spacing, type AppColors } from '../../../../theme'; +import { useChatSettingsStore } from '../../../../stores/chatSettingsStore'; +// 默认圆角,但会从 store 读取 export const BUBBLE_RADIUS = 16; -export function getBubbleColors(colors: AppColors) { +// 获取动态圆角值 +export function useBubbleRadius(): number { + return useChatSettingsStore((s) => s.messageRadius); +} + +export function getBubbleColors(colors: AppColors, outgoingBubbleColor?: string) { return { outgoing: { - background: colors.chat.bubbleOutgoing, + background: outgoingBubbleColor || colors.chat.bubbleOutgoing, text: colors.chat.textPrimary, }, incoming: { @@ -27,6 +34,76 @@ export function getBubbleColors(colors: AppColors) { export type MessageGroupPosition = 'single' | 'first' | 'middle' | 'last'; +// 使用动态圆角的 hook +export function useBubbleBorderRadius(isMe: boolean, position: MessageGroupPosition): ViewStyle { + const radius = useBubbleRadius(); + + if (isMe) { + switch (position) { + case 'single': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + borderBottomLeftRadius: radius, + borderBottomRightRadius: 4, + }; + case 'first': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + borderBottomLeftRadius: radius, + borderBottomRightRadius: 4, + }; + case 'middle': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: 4, + borderBottomLeftRadius: radius, + borderBottomRightRadius: 4, + }; + case 'last': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: 4, + borderBottomLeftRadius: radius, + borderBottomRightRadius: radius, + }; + } + } else { + switch (position) { + case 'single': + return { + borderTopLeftRadius: 4, + borderTopRightRadius: radius, + borderBottomLeftRadius: radius, + borderBottomRightRadius: radius, + }; + case 'first': + return { + borderTopLeftRadius: 4, + borderTopRightRadius: radius, + borderBottomLeftRadius: 4, + borderBottomRightRadius: radius, + }; + case 'middle': + return { + borderTopLeftRadius: 4, + borderTopRightRadius: radius, + borderBottomLeftRadius: 4, + borderBottomRightRadius: radius, + }; + case 'last': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + borderBottomLeftRadius: radius, + borderBottomRightRadius: radius, + }; + } + } +} + +// 保持原有的静态函数用于兼容 export const getBubbleBorderRadius = (isMe: boolean, position: MessageGroupPosition): ViewStyle => { const radius = BUBBLE_RADIUS; @@ -132,8 +209,8 @@ export const shouldShowSenderInfo = ( return prevMessage.sender_id !== currentMessage.sender_id; }; -export const getBubbleBaseStyle = (isMe: boolean, colors: AppColors): ViewStyle => { - const bc = getBubbleColors(colors); +export const getBubbleBaseStyle = (isMe: boolean, colors: AppColors, outgoingBubbleColor?: string): ViewStyle => { + const bc = getBubbleColors(colors, outgoingBubbleColor); return { paddingHorizontal: spacing.md, paddingVertical: spacing.sm + 4, @@ -148,19 +225,26 @@ export const getBubbleBaseStyle = (isMe: boolean, colors: AppColors): ViewStyle }; }; -export const getBubbleTextStyle = (isMe: boolean, colors: AppColors): TextStyle => { +// 使用动态字号的 hook +export function useChatFontSize(): number { + return useChatSettingsStore((s) => s.fontSize); +} + +export const getBubbleTextStyle = (isMe: boolean, colors: AppColors, customFontSize?: number): TextStyle => { const bc = getBubbleColors(colors); + const fontSize = customFontSize ?? 16; return { color: isMe ? bc.outgoing.text : bc.incoming.text, - fontSize: 16, - lineHeight: 23, + fontSize, + lineHeight: Math.round(fontSize * 1.4), letterSpacing: 0.2, fontWeight: '400', }; }; -export function createBubbleStyles(colors: AppColors) { - const bc = getBubbleColors(colors); +export function createBubbleStyles(colors: AppColors, customFontSize?: number, outgoingBubbleColor?: string) { + const bc = getBubbleColors(colors, outgoingBubbleColor); + const fontSize = customFontSize ?? 16; return StyleSheet.create({ bubble: { paddingHorizontal: spacing.md, @@ -174,8 +258,8 @@ export function createBubbleStyles(colors: AppColors) { backgroundColor: bc.incoming.background, }, text: { - fontSize: 16, - lineHeight: 23, + fontSize, + lineHeight: Math.round(fontSize * 1.4), letterSpacing: 0.2, fontWeight: '400', }, diff --git a/src/screens/message/components/ChatScreen/styles.ts b/src/screens/message/components/ChatScreen/styles.ts index d4ea101..c927bff 100644 --- a/src/screens/message/components/ChatScreen/styles.ts +++ b/src/screens/message/components/ChatScreen/styles.ts @@ -1,11 +1,12 @@ /** * ChatScreen 样式定义 - * 支持响应式布局 + * 支持响应式布局和动态聊天设置 */ import { useMemo } from 'react'; import { StyleSheet, Dimensions } from 'react-native'; import { spacing, shadows, useAppColors, type AppColors } from '../../../../theme'; +import { useChatSettingsStore, CHAT_THEMES } from '../../../../stores/chatSettingsStore'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); @@ -19,17 +20,66 @@ const BREAKPOINTS = { const isWideScreen = SCREEN_WIDTH >= BREAKPOINTS.desktop; const isTablet = SCREEN_WIDTH >= BREAKPOINTS.tablet; -export function createChatScreenStyles(colors: AppColors) { +// Hook 获取动态样式参数 +export function useChatDynamicStyles() { + const fontSize = useChatSettingsStore((s) => s.fontSize); + const messageRadius = useChatSettingsStore((s) => s.messageRadius); + const themeIndex = useChatSettingsStore((s) => s.themeIndex); + const theme = CHAT_THEMES[themeIndex]; + + return { + fontSize, + messageRadius, + // 气泡颜色 + outgoingBubbleColor: theme.bubble, + // 背景色 + chatBackgroundColor: theme.secondary, + // 卡片/头部背景色 + cardBackgroundColor: theme.card, + // 输入框背景色 + inputBackgroundColor: theme.inputBg, + // 面板背景色 + panelBackgroundColor: theme.panelBg, + // 主要文字颜色 + textPrimaryColor: theme.textPrimary, + // 次要文字颜色 + textSecondaryColor: theme.textSecondary, + }; +} + +export function createChatScreenStyles(colors: AppColors, dynamicStyles?: { + fontSize?: number; + messageRadius?: number; + outgoingBubbleColor?: string; + chatBackgroundColor?: string; + cardBackgroundColor?: string; + inputBackgroundColor?: string; + panelBackgroundColor?: string; + textPrimaryColor?: string; + textSecondaryColor?: string; +}) { + const fontSize = dynamicStyles?.fontSize ?? 16; + const messageRadius = dynamicStyles?.messageRadius ?? 12; + + // 主题颜色(优先使用主题色,否则使用默认色) + const myBubbleBg = dynamicStyles?.outgoingBubbleColor || colors.chat.bubbleOutgoing; + const screenBgColor = dynamicStyles?.chatBackgroundColor || colors.chat.screen; + const cardBgColor = dynamicStyles?.cardBackgroundColor || colors.chat.card; + const inputBgColor = dynamicStyles?.inputBackgroundColor || colors.chat.surfaceRaised; + const panelBgColor = dynamicStyles?.panelBackgroundColor || colors.chat.surfaceMuted; + const textPrimary = dynamicStyles?.textPrimaryColor || colors.chat.textPrimary; + const textSecondary = dynamicStyles?.textSecondaryColor || colors.chat.textSecondary; + return StyleSheet.create({ // 容器 container: { flex: 1, - backgroundColor: colors.chat.screen, + backgroundColor: screenBgColor, }, // 头部 headerContainer: { - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.chat.border, }, @@ -39,7 +89,7 @@ export function createChatScreenStyles(colors: AppColors) { justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.sm + 2, - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, height: 56, }, backButton: { @@ -69,7 +119,7 @@ export function createChatScreenStyles(colors: AppColors) { borderRadius: 10, alignItems: 'center', justifyContent: 'center', - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, overflow: 'hidden', }, groupAvatar: { @@ -91,13 +141,13 @@ export function createChatScreenStyles(colors: AppColors) { }, headerName: { fontWeight: '600', - color: colors.text.primary, + color: textPrimary, fontSize: 16, letterSpacing: -0.3, }, memberCount: { fontSize: 12, - color: colors.text.secondary, + color: textSecondary, fontWeight: '400', }, typingHint: { @@ -241,9 +291,9 @@ export function createChatScreenStyles(colors: AppColors) { fontWeight: '600', }, - // 消息气泡 - 微信风格:更小的圆角 + 更紧凑的内边距 + // 消息气泡 - 动态圆角支持 messageBubbleOuter: { - borderRadius: 12, + borderRadius: messageRadius, minWidth: 44, shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 1 }, @@ -252,44 +302,44 @@ export function createChatScreenStyles(colors: AppColors) { elevation: 1, }, myBubbleOuter: { - borderBottomRightRadius: 4, // 微信的小尾巴更尖 + borderBottomRightRadius: Math.max(4, messageRadius * 0.3), // 微信的小尾巴更尖 }, theirBubbleOuter: { - borderTopLeftRadius: 4, + borderTopLeftRadius: Math.max(4, messageRadius * 0.3), }, - // 内层:更紧凑的微信风格内边距 + // 内层:动态圆角支持 messageBubbleInner: { - borderRadius: 12, + borderRadius: messageRadius, overflow: 'hidden', paddingHorizontal: 10, // 微信:更窄的左右边距 paddingVertical: 7, // 微信:更紧的上下边距 minWidth: 44, }, myBubbleInner: { - backgroundColor: colors.chat.bubbleOutgoing, - borderBottomRightRadius: 4, + backgroundColor: myBubbleBg, + borderBottomRightRadius: Math.max(4, messageRadius * 0.3), }, theirBubbleInner: { - backgroundColor: colors.chat.bubbleIncoming, - borderTopLeftRadius: 4, + backgroundColor: cardBgColor, + borderTopLeftRadius: Math.max(4, messageRadius * 0.3), }, recalledBubble: { - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, borderWidth: 1, borderColor: colors.chat.border, borderStyle: 'dashed', }, myBubbleText: { - color: colors.chat.textPrimary, // 使用主题文字颜色 - fontSize: 18, - lineHeight: 25, + color: textPrimary, // 使用主题文字颜色 + fontSize, + lineHeight: Math.round(fontSize * 1.4), letterSpacing: 0.1, fontWeight: '400', }, theirBubbleText: { - color: colors.chat.textPrimary, // 使用主题文字颜色 - fontSize: 18, - lineHeight: 25, + color: textPrimary, // 使用主题文字颜色 + fontSize, + lineHeight: Math.round(fontSize * 1.4), letterSpacing: 0.1, fontWeight: '400', }, @@ -362,7 +412,7 @@ export function createChatScreenStyles(colors: AppColors) { // 输入框区域 - 与列表背景同色底栏,顶部细分隔 inputWrapper: { - backgroundColor: colors.chat.screen, + backgroundColor: screenBgColor, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: 'rgba(0, 0, 0, 0.06)', }, @@ -384,7 +434,7 @@ export function createChatScreenStyles(colors: AppColors) { inputInner: { flexDirection: 'row', alignItems: 'center', - backgroundColor: colors.chat.surfaceRaised, + backgroundColor: inputBgColor, borderRadius: 20, paddingHorizontal: spacing.xs, paddingVertical: 4, @@ -518,7 +568,7 @@ export function createChatScreenStyles(colors: AppColors) { // 面板 panelWrapper: { - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, borderTopWidth: 1, borderTopColor: colors.chat.border, shadowColor: colors.chat.shadow, @@ -529,11 +579,11 @@ export function createChatScreenStyles(colors: AppColors) { }, // 表情面板包装器 - 底部 tab 栏是白色,安全区域也用白色填充 emojiPanelWrapper: { - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, }, panelContainer: { flex: 1, - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, }, // 表情面板 @@ -578,12 +628,12 @@ export function createChatScreenStyles(colors: AppColors) { paddingVertical: spacing.sm + 2, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: colors.chat.borderHairline, - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, }, panelTabContainer: { flexDirection: 'row', alignItems: 'center', - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, borderRadius: 12, padding: 3, }, @@ -595,7 +645,7 @@ export function createChatScreenStyles(colors: AppColors) { borderRadius: 10, }, panelTabActive: { - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.08, @@ -617,7 +667,7 @@ export function createChatScreenStyles(colors: AppColors) { alignItems: 'center', justifyContent: 'center', borderRadius: 10, - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, }, // 自定义表情面板 @@ -656,7 +706,7 @@ export function createChatScreenStyles(colors: AppColors) { margin: 4, borderRadius: 8, overflow: 'hidden', - backgroundColor: colors.chat.surfaceInput, + backgroundColor: inputBgColor, }, stickerImage: { width: '100%', @@ -684,7 +734,7 @@ export function createChatScreenStyles(colors: AppColors) { alignItems: 'center', justifyContent: 'center', marginBottom: spacing.sm + 2, - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, shadowColor: colors.chat.shadow, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.06, @@ -709,14 +759,14 @@ export function createChatScreenStyles(colors: AppColors) { }, moreItemText: { fontSize: 12, - color: colors.chat.textSecondary, + color: textSecondary, fontWeight: '500', letterSpacing: 0.3, }, // MorePanel 整体背景 morePanelBackground: { flex: 1, - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, }, // @成员选择浮层 - 绝对定位浮在输入框上方 @@ -724,7 +774,7 @@ export function createChatScreenStyles(colors: AppColors) { position: 'absolute', left: 12, right: 12, - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderRadius: 18, shadowColor: colors.primary.main, shadowOffset: { width: 0, height: -3 }, @@ -739,7 +789,7 @@ export function createChatScreenStyles(colors: AppColors) { // @成员选择面板 mentionPanelContainer: { flex: 1, - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderRadius: 18, }, mentionPanelDragHandle: { @@ -774,7 +824,7 @@ export function createChatScreenStyles(colors: AppColors) { width: 26, height: 26, borderRadius: 13, - backgroundColor: colors.chat.surfaceInput, + backgroundColor: inputBgColor, alignItems: 'center', justifyContent: 'center', }, @@ -851,7 +901,7 @@ export function createChatScreenStyles(colors: AppColors) { }, // 旧版菜单样式(保留兼容) menuContainer: { - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderRadius: 12, minWidth: 160, shadowColor: colors.chat.shadow, @@ -873,12 +923,12 @@ export function createChatScreenStyles(colors: AppColors) { }, menuItemText: { fontSize: 15, - color: colors.text.primary, + color: textPrimary, fontWeight: '500', }, // 底部菜单容器 bottomMenuContainer: { - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingTop: spacing.md, @@ -891,7 +941,7 @@ export function createChatScreenStyles(colors: AppColors) { }, // 消息预览区域 messagePreviewContainer: { - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, marginHorizontal: spacing.md, marginBottom: spacing.md, paddingHorizontal: spacing.md, @@ -934,12 +984,12 @@ export function createChatScreenStyles(colors: AppColors) { }, menuActionLabel: { fontSize: 13, - color: colors.text.primary, + color: textPrimary, fontWeight: '500', }, // 取消按钮 menuCancelButton: { - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, marginHorizontal: spacing.md, paddingVertical: spacing.md, borderRadius: 12, @@ -947,7 +997,7 @@ export function createChatScreenStyles(colors: AppColors) { }, menuCancelText: { fontSize: 16, - color: colors.text.primary, + color: textPrimary, fontWeight: '600', }, @@ -1037,7 +1087,7 @@ export function createChatScreenStyles(colors: AppColors) { zIndex: 1000, }, overlayContent: { - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, padding: spacing.xl, borderRadius: 16, alignItems: 'center', @@ -1056,7 +1106,7 @@ export function createChatScreenStyles(colors: AppColors) { // 表情包添加按钮(管理界面第一位) stickerAddButton: { - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderWidth: 1.5, borderColor: colors.primary.main, borderStyle: 'dashed', @@ -1075,7 +1125,7 @@ export function createChatScreenStyles(colors: AppColors) { // 表情包管理按钮(表情面板第一位) stickerManageButton: { - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderWidth: 1, borderColor: colors.chat.border, borderStyle: 'dashed', @@ -1146,7 +1196,7 @@ export function createChatScreenStyles(colors: AppColors) { // 表情管理模态框 manageModalContainer: { - backgroundColor: colors.chat.surfaceMuted, + backgroundColor: panelBgColor, borderTopLeftRadius: 20, borderTopRightRadius: 20, overflow: 'hidden', @@ -1157,7 +1207,7 @@ export function createChatScreenStyles(colors: AppColors) { justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.md, - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderBottomWidth: 1, borderBottomColor: colors.chat.border, }, @@ -1183,7 +1233,7 @@ export function createChatScreenStyles(colors: AppColors) { manageInfoBar: { paddingHorizontal: spacing.md, paddingVertical: spacing.sm, - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderBottomWidth: 1, borderBottomColor: colors.chat.border, }, @@ -1204,7 +1254,7 @@ export function createChatScreenStyles(colors: AppColors) { justifyContent: 'space-between', paddingHorizontal: spacing.md, paddingVertical: spacing.md, - backgroundColor: colors.chat.card, + backgroundColor: cardBgColor, borderTopWidth: 1, borderTopColor: colors.chat.border, paddingBottom: 34, // 安全区域 @@ -1279,5 +1329,6 @@ export function createChatScreenStyles(colors: AppColors) { export function useChatScreenStyles() { const colors = useAppColors(); - return useMemo(() => createChatScreenStyles(colors), [colors]); + const dynamicStyles = useChatDynamicStyles(); + return useMemo(() => createChatScreenStyles(colors, dynamicStyles), [colors, dynamicStyles]); } diff --git a/src/screens/profile/ChatSettingsScreen.tsx b/src/screens/profile/ChatSettingsScreen.tsx index 926250a..f8ed3ff 100644 --- a/src/screens/profile/ChatSettingsScreen.tsx +++ b/src/screens/profile/ChatSettingsScreen.tsx @@ -3,7 +3,7 @@ * 胡萝卜BBS - 聊天个性化设置 */ -import React, { useMemo, useState, useCallback } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { View, StyleSheet, @@ -23,19 +23,16 @@ import { } from '../../theme'; import { Text } from '../../components/common'; import { useResponsiveSpacing } from '../../hooks'; +import { + useChatSettingsStore, + useChatSettingsActions, + CHAT_THEMES, +} from '../../stores/chatSettingsStore'; +import { useCurrentUser } from '../../stores'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const CARD_MAX_WIDTH = 720; -// 主题配置 -const THEMES = [ - { id: 'default', name: '默认绿', primary: '#4CAF50', secondary: '#E8F5E9', bubble: '#DCF8C6', icon: '🏠' }, - { id: 'yellow', name: '活力黄', primary: '#FFC107', secondary: '#FFF8E1', bubble: '#FFECB3', icon: '🐥' }, - { id: 'blue', name: '清新蓝', primary: '#2196F3', secondary: '#E3F2FD', bubble: '#BBDEFB', icon: '☃️' }, - { id: 'purple', name: '梦幻紫', primary: '#9C27B0', secondary: '#F3E5F5', bubble: '#E1BEE7', icon: '💎' }, - { id: 'teal', name: '自然青', primary: '#009688', secondary: '#E0F2F1', bubble: '#B2DFDB', icon: '🦁' }, -] as const; - interface SliderProps { value: number; min: number; @@ -306,13 +303,22 @@ export const ChatSettingsScreen: React.FC = () => { const styles = useMemo(() => createStyles(colors), [colors]); const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); - // 状态 - const [fontSize, setFontSize] = useState(16); - const [messageRadius, setMessageRadius] = useState(17); - const [selectedTheme, setSelectedTheme] = useState(0); - const [nightMode, setNightMode] = useState(false); + // 获取当前用户信息 + const currentUser = useCurrentUser(); - const currentTheme = THEMES[selectedTheme]; + // 从 store 获取状态 + const fontSize = useChatSettingsStore((s) => s.fontSize); + const messageRadius = useChatSettingsStore((s) => s.messageRadius); + const themeIndex = useChatSettingsStore((s) => s.themeIndex); + const nightMode = useChatSettingsStore((s) => s.nightMode); + + // 获取操作方法 + const { setFontSize, setMessageRadius, setTheme, toggleNightMode } = useChatSettingsActions(); + + const currentTheme = CHAT_THEMES[themeIndex]; + + // 用户显示名称 + const userName = currentUser?.nickname || currentUser?.username || '我'; // 渲染聊天预览 const renderChatPreview = () => ( @@ -320,7 +326,7 @@ export const ChatSettingsScreen: React.FC = () => { 预览 - MiMQy + {userName} 早上好!👋 @@ -331,9 +337,9 @@ export const ChatSettingsScreen: React.FC = () => { - 现在是东京的早晨😎 + 现在是威海的早晨😎 - AM12:57 ✓ + AM12:57 @@ -365,15 +371,15 @@ export const ChatSettingsScreen: React.FC = () => { showsHorizontalScrollIndicator={false} contentContainerStyle={styles.themeList} > - {THEMES.map((theme, index) => ( + {CHAT_THEMES.map((theme, index) => ( setSelectedTheme(index)} + onPress={() => setTheme(index)} activeOpacity={0.8} > @@ -450,7 +456,7 @@ export const ChatSettingsScreen: React.FC = () => { setNightMode(!nightMode)} + onPress={() => toggleNightMode()} > diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 8b26624..a6398a6 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -16,6 +16,7 @@ import { User } from '../types'; import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../services'; import { wsService } from '../services/wsService'; import { callStore } from './callStore'; +import { useSessionStore } from './sessionStore'; import { initDatabase, closeDatabase, @@ -149,6 +150,9 @@ export const useAuthStore = create((set) => ({ // 4. 写用户缓存(DB 已就绪) await cacheUser(user); + // 5. 同步 userId 到 sessionStore + useSessionStore.getState().setUserId(userId); + set({ isAuthenticated: true, currentUser: user, @@ -156,7 +160,7 @@ export const useAuthStore = create((set) => ({ error: null, }); - // 5. 启动 SSE + // 6. 启动 SSE await startRealtime(); return true; @@ -187,6 +191,9 @@ export const useAuthStore = create((set) => ({ await saveUserId(userId); await cacheUser(user); + // 同步 userId 到 sessionStore + useSessionStore.getState().setUserId(userId); + set({ isAuthenticated: true, currentUser: user, @@ -223,7 +230,9 @@ export const useAuthStore = create((set) => ({ await closeDatabase(); // 6. 清除持久化的 userId await clearUserId(); - // 7. 清除 userManager 的内存缓存 + // 7. 清除 sessionStore 的 userId + useSessionStore.getState().setUserId(null); + // 8. 清除 userManager 的内存缓存 const { userManager } = await import('./userManager'); userManager.invalidateUsers(); } catch (error) { @@ -272,17 +281,21 @@ export const useAuthStore = create((set) => ({ // 5. DB 已就绪,更新用户缓存 await cacheUser(user); + // 6. 同步 userId 到 sessionStore + useSessionStore.getState().setUserId(userId); + set({ isAuthenticated: true, currentUser: user, isLoading: false, }); - // 6. 启动 SSE + // 7. 启动 SSE await startRealtime(); } else { // Token 已失效或不存在 await clearUserId(); + useSessionStore.getState().setUserId(null); set({ isAuthenticated: false, currentUser: null, diff --git a/src/stores/callStore.ts b/src/stores/callStore.ts index aac6371..0aa23e9 100644 --- a/src/stores/callStore.ts +++ b/src/stores/callStore.ts @@ -8,7 +8,7 @@ import { WSErrorMessage, } from '../services/wsService'; import { webrtcManager, ICEServer } from '../services/webrtc'; -import { useAuthStore } from './authStore'; +import { getCurrentUserId } from './sessionStore'; import { useUserStore } from './userStore'; import { userManager } from './userManager'; @@ -444,7 +444,7 @@ export const callStore = create((set, get) => ({ callTimeoutTimer = null; } - const myUserId = useAuthStore.getState().currentUser?.id || ''; + const myUserId = getCurrentUserId() || ''; try { const isVideoCall = currentCall.callType === 'video'; @@ -555,7 +555,7 @@ export const callStore = create((set, get) => ({ const { currentCall } = get(); if (!currentCall || currentCall.id !== msg.call_id) return; - const myUserId = useAuthStore.getState().currentUser?.id || ''; + const myUserId = getCurrentUserId() || ''; if (msg.from_id === myUserId) return; if (msg.payload.sdp_type === 'offer') { @@ -571,7 +571,7 @@ export const callStore = create((set, get) => ({ const { currentCall } = get(); if (!currentCall || currentCall.id !== msg.call_id) return; - const myUserId = useAuthStore.getState().currentUser?.id || ''; + const myUserId = getCurrentUserId() || ''; if (msg.from_id === myUserId) return; try { @@ -616,7 +616,7 @@ export const callStore = create((set, get) => ({ return; } - const myUserId = useAuthStore.getState().currentUser?.id; + const myUserId = getCurrentUserId(); if (!myUserId) { console.error('[CallStore] Not logged in'); return; @@ -681,7 +681,7 @@ export const callStore = create((set, get) => ({ const isVideoCall = incomingCall.callType === 'video'; console.log('[CallStore] acceptCall, isVideoCall:', isVideoCall); - const myUserId = useAuthStore.getState().currentUser?.id || ''; + const myUserId = getCurrentUserId() || ''; set({ currentCall: { diff --git a/src/stores/chatSettingsStore.ts b/src/stores/chatSettingsStore.ts new file mode 100644 index 0000000..f92fcea --- /dev/null +++ b/src/stores/chatSettingsStore.ts @@ -0,0 +1,201 @@ +/** + * 聊天设置 Store + * 持久化存储用户的聊天样式偏好设置 + */ + +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import { useShallow } from 'zustand/react/shallow'; + +// 主题配置 - 扩展包含更多颜色 +export const CHAT_THEMES = [ + { + id: 'default', + name: '默认绿', + primary: '#4CAF50', + secondary: '#E8F5E9', + bubble: '#DCF8C6', + icon: '🏠', + // 扩展颜色 + card: '#FFFFFF', // 卡片背景 + inputBg: '#FFFFFF', // 输入框背景 + panelBg: '#F5F5F5', // 面板背景 + headerBg: '#FFFFFF', // 头部背景 + textPrimary: '#1A1A1A', // 主要文字 + textSecondary: '#666666', // 次要文字 + }, + { + id: 'yellow', + name: '活力黄', + primary: '#FFC107', + secondary: '#FFF8E1', + bubble: '#FFECB3', + icon: '🐥', + card: '#FFFFFF', + inputBg: '#FFFFFF', + panelBg: '#FFFDE7', + headerBg: '#FFFFFF', + textPrimary: '#1A1A1A', + textSecondary: '#666666', + }, + { + id: 'blue', + name: '清新蓝', + primary: '#2196F3', + secondary: '#E3F2FD', + bubble: '#BBDEFB', + icon: '☃️', + card: '#FFFFFF', + inputBg: '#FFFFFF', + panelBg: '#E1F5FE', + headerBg: '#FFFFFF', + textPrimary: '#1A1A1A', + textSecondary: '#666666', + }, + { + id: 'purple', + name: '梦幻紫', + primary: '#9C27B0', + secondary: '#F3E5F5', + bubble: '#E1BEE7', + icon: '💎', + card: '#FFFFFF', + inputBg: '#FFFFFF', + panelBg: '#F3E5F5', + headerBg: '#FFFFFF', + textPrimary: '#1A1A1A', + textSecondary: '#666666', + }, + { + id: 'teal', + name: '自然青', + primary: '#009688', + secondary: '#E0F2F1', + bubble: '#B2DFDB', + icon: '🦁', + card: '#FFFFFF', + inputBg: '#FFFFFF', + panelBg: '#E0F2F1', + headerBg: '#FFFFFF', + textPrimary: '#1A1A1A', + textSecondary: '#666666', + }, +] as const; + +export type ChatThemeId = typeof CHAT_THEMES[number]['id']; + +export interface ChatSettings { + // 消息字号 (12-24) + fontSize: number; + // 消息圆角 (0-30) + messageRadius: number; + // 选中的主题索引 + themeIndex: number; + // 夜间模式 + nightMode: boolean; + // 聊天壁纸 (可选) + wallpaper?: string | null; + // 名称颜色 (可选) + nameColor?: string | null; +} + +const DEFAULT_SETTINGS: ChatSettings = { + fontSize: 16, + messageRadius: 12, + themeIndex: 0, + nightMode: false, + wallpaper: null, + nameColor: null, +}; + +interface ChatSettingsState extends ChatSettings { + // 获取当前主题 + getCurrentTheme: () => typeof CHAT_THEMES[number]; + // 更新设置 + updateSettings: (settings: Partial) => void; + // 重置为默认 + resetSettings: () => void; + // 设置字号 + setFontSize: (size: number) => void; + // 设置圆角 + setMessageRadius: (radius: number) => void; + // 设置主题 + setTheme: (index: number) => void; + // 切换夜间模式 + toggleNightMode: () => void; +} + +const STORAGE_KEY = 'chat_settings'; + +export const useChatSettingsStore = create()( + persist( + (set, get) => ({ + ...DEFAULT_SETTINGS, + + getCurrentTheme: () => { + const { themeIndex } = get(); + return CHAT_THEMES[themeIndex] ?? CHAT_THEMES[0]; + }, + + updateSettings: (settings) => { + set((state) => ({ ...state, ...settings })); + }, + + resetSettings: () => { + set(DEFAULT_SETTINGS); + }, + + setFontSize: (fontSize) => { + set({ fontSize: Math.max(12, Math.min(24, fontSize)) }); + }, + + setMessageRadius: (messageRadius) => { + set({ messageRadius: Math.max(0, Math.min(30, messageRadius)) }); + }, + + setTheme: (themeIndex) => { + set({ themeIndex: Math.max(0, Math.min(CHAT_THEMES.length - 1, themeIndex)) }); + }, + + toggleNightMode: () => { + set((state) => ({ nightMode: !state.nightMode })); + }, + }), + { + name: STORAGE_KEY, + storage: createJSONStorage(() => AsyncStorage), + } + ) +); + +// 导出便捷 hooks +export function useChatFontSize() { + return useChatSettingsStore((s) => s.fontSize); +} + +export function useChatMessageRadius() { + return useChatSettingsStore((s) => s.messageRadius); +} + +export function useChatTheme() { + const themeIndex = useChatSettingsStore((s) => s.themeIndex); + return CHAT_THEMES[themeIndex]; +} + +export function useChatNightMode() { + return useChatSettingsStore((s) => s.nightMode); +} + +export function useChatSettingsActions() { + return useChatSettingsStore( + useShallow((s) => ({ + setFontSize: s.setFontSize, + setMessageRadius: s.setMessageRadius, + setTheme: s.setTheme, + toggleNightMode: s.toggleNightMode, + updateSettings: s.updateSettings, + resetSettings: s.resetSettings, + })) + ); +} diff --git a/src/stores/index.ts b/src/stores/index.ts index c384553..c71cd69 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -3,6 +3,19 @@ */ export { useAuthStore, useCurrentUser, useIsAuthenticated, useAuthLoading } from './authStore'; + +// 聊天设置 Store +export { + useChatSettingsStore, + useChatSettingsActions, + useChatFontSize, + useChatMessageRadius, + useChatTheme, + useChatNightMode, + CHAT_THEMES, +} from './chatSettingsStore'; +export type { ChatSettings, ChatThemeId } from './chatSettingsStore'; + export { useUserStore, usePosts, diff --git a/src/stores/sessionStore.ts b/src/stores/sessionStore.ts new file mode 100644 index 0000000..10f6ac4 --- /dev/null +++ b/src/stores/sessionStore.ts @@ -0,0 +1,27 @@ +/** + * 会话状态管理 + * + * 这是一个轻量级的 store,只存储当前用户的身份信息(userId)。 + * 它的目的是打破 authStore 和 callStore 之间的循环依赖: + * + * sessionStore (存储 userId) + * ↑ ↑ + * authStore callStore + * + * authStore 在登录成功后设置 userId,callStore 从 sessionStore 读取 userId。 + */ + +import { create } from 'zustand'; + +interface SessionState { + userId: string | null; + setUserId: (userId: string | null) => void; +} + +export const useSessionStore = create((set) => ({ + userId: null, + setUserId: (userId) => set({ userId }), +})); + +// 导出便捷函数,用于非 React 组件中直接获取 userId +export const getCurrentUserId = () => useSessionStore.getState().userId; \ No newline at end of file