refactor(message): improve message synchronization and hook reliability
Some checks failed
Frontend CI / ota-android (push) Successful in 1m29s
Frontend CI / build-and-push-web (push) Failing after 1m31s
Frontend CI / ota-ios (push) Successful in 4m4s
Frontend CI / build-android-apk (push) Successful in 29m5s

Refactor the message management system to address synchronization issues and improve performance through request deduplication and enhanced lifecycle management.

- Implement in-flight promise tracking in `MessageSyncService` to prevent redundant network requests for conversations and messages.
- Enhance `useMessages` hook with `useFocusEffect` to trigger automatic synchronization when a chat screen regains focus.
- Add `forceSync` capability to `MessageManager` and `MessageSyncService` to allow manual overrides of loading states during re-entry.
- Consolidate WebSocket synchronization logic in `WSMessageHandler` using a throttled and deduplicated trigger mechanism.
- Clean up unused styles and deprecated hooks (`baseHooks.ts`, `bubbleStyles.ts`, `inputStyles.ts`).
- Update `metro.config.js` and `package.json` to include `@expo/ui` and optimize resolver settings.
This commit is contained in:
2026-06-03 10:31:46 +08:00
parent 2e6912dddf
commit 2e2f6e3467
13 changed files with 396 additions and 1115 deletions

View File

@@ -1,317 +0,0 @@
/**
* 消息气泡样式工具
* 参考 Element X 设计,实现动态圆角和现代化气泡样式
*/
import { StyleSheet, ViewStyle, TextStyle } from 'react-native';
import { spacing, type AppColors } from '../../../../theme';
import { useChatSettingsStore } from '../../../../stores/settings';
// 默认圆角,但会从 store 读取
export const BUBBLE_RADIUS = 16;
// 获取动态圆角值
export function useBubbleRadius(): number {
return useChatSettingsStore((s) => s.messageRadius);
}
export function getBubbleColors(colors: AppColors, outgoingBubbleColor?: string) {
return {
outgoing: {
background: outgoingBubbleColor || colors.chat.bubbleOutgoing,
text: colors.chat.textPrimary,
},
incoming: {
background: colors.chat.bubbleIncoming,
text: colors.chat.textPrimary,
},
replyHighlight: {
background: colors.chat.replyTint,
borderLeft: colors.chat.replyBorder,
},
};
}
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;
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 getMessageGroupPosition = (
index: number,
messages: Array<{ sender_id: string }>,
currentUserId: string
): MessageGroupPosition => {
const currentMessage = messages[index];
if (!currentMessage) return 'single';
const prevMessage = index > 0 ? messages[index - 1] : null;
const nextMessage = index < messages.length - 1 ? messages[index + 1] : null;
const isSameSenderAsPrev = prevMessage && prevMessage.sender_id === currentMessage.sender_id;
const isSameSenderAsNext = nextMessage && nextMessage.sender_id === currentMessage.sender_id;
if (!isSameSenderAsPrev && !isSameSenderAsNext) {
return 'single';
} else if (!isSameSenderAsPrev && isSameSenderAsNext) {
return 'first';
} else if (isSameSenderAsPrev && isSameSenderAsNext) {
return 'middle';
} else {
return 'last';
}
};
export const shouldShowSenderInfo = (
index: number,
messages: Array<{ sender_id: string }>
): boolean => {
if (index === 0) return true;
const currentMessage = messages[index];
const prevMessage = messages[index - 1];
return prevMessage.sender_id !== currentMessage.sender_id;
};
export const getBubbleBaseStyle = (isMe: boolean, colors: AppColors, outgoingBubbleColor?: string): ViewStyle => {
const bc = getBubbleColors(colors, outgoingBubbleColor);
return {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm + 4,
minWidth: 60,
maxWidth: '75%',
backgroundColor: isMe ? bc.outgoing.background : bc.incoming.background,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.06,
shadowRadius: 2,
elevation: 2,
};
};
// 使用动态字号的 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,
lineHeight: Math.round(fontSize * 1.4),
letterSpacing: 0.2,
fontWeight: '400',
};
};
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,
paddingVertical: spacing.sm + 4,
minWidth: 60,
},
outgoing: {
backgroundColor: bc.outgoing.background,
},
incoming: {
backgroundColor: bc.incoming.background,
},
text: {
fontSize,
lineHeight: Math.round(fontSize * 1.4),
letterSpacing: 0.2,
fontWeight: '400',
},
shadow: {
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.06,
shadowRadius: 2,
elevation: 2,
},
longPressShadow: {
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.12,
shadowRadius: 8,
elevation: 6,
},
replyHighlight: {
borderLeftWidth: 3,
borderLeftColor: colors.primary.main,
backgroundColor: bc.replyHighlight.background,
},
recalled: {
backgroundColor: colors.chat.surfaceMuted,
borderWidth: 1,
borderColor: colors.chat.border,
borderStyle: 'dashed',
borderRadius: 12,
},
systemNotice: {
alignItems: 'center',
marginVertical: spacing.sm,
},
systemNoticeText: {
fontSize: 12,
color: colors.chat.textSecondary,
backgroundColor: colors.chat.surfaceMuted,
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: 12,
overflow: 'hidden',
},
});
}
export default {
BUBBLE_RADIUS,
getBubbleColors,
getBubbleBorderRadius,
getMessageGroupPosition,
shouldShowSenderInfo,
getBubbleBaseStyle,
getBubbleTextStyle,
createBubbleStyles,
};

View File

@@ -1,4 +1,4 @@
/**
/**
* 聊天设置页 ChatSettingsScreen
* 威友 - 聊天个性化设置
*/
@@ -9,7 +9,6 @@ import {
StyleSheet,
TouchableOpacity,
ScrollView,
Dimensions,
PanResponder,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
@@ -30,7 +29,6 @@ import {
CHAT_THEMES,
} from '../../stores/settings';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const CARD_MAX_WIDTH = 720;
interface SliderProps {
@@ -43,7 +41,6 @@ interface SliderProps {
showValue?: boolean;
}
// 自定义滑块组件(支持拖动)
const Slider: React.FC<SliderProps> = ({
value,
min,
@@ -56,13 +53,13 @@ const Slider: React.FC<SliderProps> = ({
const colors = useAppColors();
const styles = useMemo(() => createSliderStyles(colors), [colors]);
const trackWidthRef = useRef(0);
const percentage = isNaN(value) ? 0 : ((value - min) / (max - min)) * 100;
const updateValue = useCallback((locationX: number) => {
const width = trackWidthRef.current;
if (width <= 0) return;
const newPercentage = Math.max(0, Math.min(100, (locationX / width) * 100));
const newValue = Math.round(min + (newPercentage / 100) * (max - min));
onValueChange(newValue);
@@ -90,7 +87,7 @@ const Slider: React.FC<SliderProps> = ({
{showValue && <Text style={styles.value}>{isNaN(value) ? 0 : value}</Text>}
{rightLabel && <Text style={styles.label}>{rightLabel}</Text>}
</View>
<View
<View
style={styles.trackContainer}
onLayout={handleLayout}
{...panResponder.panHandlers}
@@ -177,7 +174,6 @@ function createStyles(colors: AppColors) {
textTransform: 'uppercase',
letterSpacing: 0.5,
},
// 聊天预览样式
previewContainer: {
backgroundColor: colors.primary.light + '20',
borderRadius: 14,
@@ -211,7 +207,6 @@ function createStyles(colors: AppColors) {
previewText: {
color: colors.text.primary,
},
// 主题选择样式
themeList: {
flexDirection: 'row',
gap: spacing.sm,
@@ -254,12 +249,6 @@ function createStyles(colors: AppColors) {
justifyContent: 'center',
alignItems: 'center',
},
themeName: {
fontSize: fontSizes.xs,
textAlign: 'center',
marginTop: 4,
color: colors.text.secondary,
},
sliderContainer: {
paddingVertical: 8,
},
@@ -272,7 +261,6 @@ export const ChatSettingsScreen: React.FC = () => {
const styles = useMemo(() => createStyles(colors), [colors]);
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
// 从 store 获取状态
const fontSize = useChatSettingsStore((s) => s.fontSize);
const messageRadius = useChatSettingsStore((s) => s.messageRadius);
const themeIndex = useChatSettingsStore((s) => s.themeIndex);
@@ -281,14 +269,13 @@ export const ChatSettingsScreen: React.FC = () => {
const currentTheme = CHAT_THEMES[themeIndex];
// 渲染聊天预览
const renderChatPreview = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={[styles.previewContainer, { backgroundColor: currentTheme.secondary }]}>
<View style={[
styles.previewMessage,
{
styles.previewMessage,
{
borderRadius: messageRadius,
borderTopLeftRadius: Math.max(4, messageRadius * 0.3),
}
@@ -301,11 +288,11 @@ export const ChatSettingsScreen: React.FC = () => {
</Text>
</View>
<View style={[
styles.previewReply,
{
styles.previewReply,
{
borderRadius: messageRadius,
borderBottomRightRadius: Math.max(4, messageRadius * 0.3),
backgroundColor: currentTheme.bubble
backgroundColor: currentTheme.bubble
}
]}>
<Text style={[styles.previewText, { fontSize }]}>
@@ -316,7 +303,6 @@ export const ChatSettingsScreen: React.FC = () => {
</View>
);
// 渲染字号滑块
const renderFontSizeSlider = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
@@ -333,12 +319,11 @@ export const ChatSettingsScreen: React.FC = () => {
</View>
);
// 渲染主题颜色选择
const renderThemeColors = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<ScrollView
horizontal
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.themeList}
>
@@ -366,7 +351,6 @@ export const ChatSettingsScreen: React.FC = () => {
</View>
);
// 渲染圆角滑块
const renderRadiusSlider = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
@@ -385,9 +369,9 @@ export const ChatSettingsScreen: React.FC = () => {
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="聊天设置" onBack={() => router.back()} />
<ScrollView
<ScrollView
contentContainerStyle={[
styles.scrollContent,
styles.scrollContent,
{ paddingHorizontal: responsivePadding, paddingBottom: 80 }
]}
>

View File

@@ -115,7 +115,7 @@ export const VerificationSettingsScreen: React.FC = () => {
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 状态卡片 */}
{/* 状态卡片 */}
<View style={styles.statusCard}>
<View style={[styles.statusIconContainer, { backgroundColor: statusConfig.color + '20' }]}>
<MaterialCommunityIcons