- Remove legacy database.ts, LocalDataSource.ts, and MessageRepository.ts - Create new src/database/ module with messageRepository, userCacheRepository, conversationRepository, and groupCacheRepository - Update all consumers to import from @/database instead of services/database - Add web platform blur handling for modal components to fix focus issues - Flatten SystemMessageItem and NotificationsScreen styles for consistent design - Add draggable slider in ChatSettingsScreen and dynamic font size support - Introduce 9 new chat color themes - Add profile screens for about, terms, and privacy policy with navigation routes - Add policy links to login and registration screens - Fix post share URL format from /posts/ to /post/
399 lines
10 KiB
TypeScript
399 lines
10 KiB
TypeScript
/**
|
||
* 聊天设置页 ChatSettingsScreen
|
||
* 胡萝卜BBS - 聊天个性化设置
|
||
*/
|
||
|
||
import React, { useMemo, useCallback, useRef } from 'react';
|
||
import {
|
||
View,
|
||
StyleSheet,
|
||
TouchableOpacity,
|
||
ScrollView,
|
||
Dimensions,
|
||
PanResponder,
|
||
} from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
import {
|
||
useAppColors,
|
||
spacing,
|
||
fontSizes,
|
||
borderRadius,
|
||
type AppColors,
|
||
} from '../../theme';
|
||
import { Text } from '../../components/common';
|
||
import { useResponsiveSpacing } from '../../hooks';
|
||
import {
|
||
useChatSettingsStore,
|
||
useChatSettingsActions,
|
||
CHAT_THEMES,
|
||
} from '../../stores/chatSettingsStore';
|
||
|
||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||
const CARD_MAX_WIDTH = 720;
|
||
|
||
interface SliderProps {
|
||
value: number;
|
||
min: number;
|
||
max: number;
|
||
onValueChange: (value: number) => void;
|
||
leftLabel?: string;
|
||
rightLabel?: string;
|
||
showValue?: boolean;
|
||
}
|
||
|
||
// 自定义滑块组件(支持拖动)
|
||
const Slider: React.FC<SliderProps> = ({
|
||
value,
|
||
min,
|
||
max,
|
||
onValueChange,
|
||
leftLabel,
|
||
rightLabel,
|
||
showValue = true,
|
||
}) => {
|
||
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);
|
||
}, [min, max, onValueChange]);
|
||
|
||
const panResponder = useMemo(() => PanResponder.create({
|
||
onStartShouldSetPanResponder: () => true,
|
||
onMoveShouldSetPanResponder: () => true,
|
||
onPanResponderGrant: (evt) => {
|
||
updateValue(evt.nativeEvent.locationX);
|
||
},
|
||
onPanResponderMove: (evt) => {
|
||
updateValue(evt.nativeEvent.locationX);
|
||
},
|
||
}), [updateValue]);
|
||
|
||
const handleLayout = useCallback((event: any) => {
|
||
trackWidthRef.current = event.nativeEvent.layout.width;
|
||
}, []);
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<View style={styles.labelRow}>
|
||
{leftLabel && <Text style={styles.label}>{leftLabel}</Text>}
|
||
{showValue && <Text style={styles.value}>{isNaN(value) ? 0 : value}</Text>}
|
||
{rightLabel && <Text style={styles.label}>{rightLabel}</Text>}
|
||
</View>
|
||
<View
|
||
style={styles.trackContainer}
|
||
onLayout={handleLayout}
|
||
{...panResponder.panHandlers}
|
||
>
|
||
<View style={styles.track}>
|
||
<View style={[styles.fill, { width: `${percentage}%`, backgroundColor: colors.primary.main }]} />
|
||
</View>
|
||
<View style={[styles.thumb, { left: `${percentage}%`, backgroundColor: colors.primary.main }]} />
|
||
</View>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
function createSliderStyles(colors: AppColors) {
|
||
return StyleSheet.create({
|
||
container: {
|
||
marginVertical: spacing.sm,
|
||
},
|
||
labelRow: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: spacing.xs,
|
||
},
|
||
label: {
|
||
fontSize: fontSizes.sm,
|
||
color: colors.text.secondary,
|
||
},
|
||
value: {
|
||
fontSize: fontSizes.md,
|
||
fontWeight: '600',
|
||
color: colors.primary.main,
|
||
},
|
||
trackContainer: {
|
||
height: 24,
|
||
justifyContent: 'center',
|
||
},
|
||
track: {
|
||
height: 4,
|
||
backgroundColor: colors.divider,
|
||
borderRadius: 2,
|
||
},
|
||
fill: {
|
||
height: '100%',
|
||
borderRadius: 2,
|
||
},
|
||
thumb: {
|
||
position: 'absolute',
|
||
width: 20,
|
||
height: 20,
|
||
borderRadius: 10,
|
||
marginLeft: -10,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.2,
|
||
shadowRadius: 2,
|
||
elevation: 3,
|
||
},
|
||
});
|
||
}
|
||
|
||
function createStyles(colors: AppColors) {
|
||
return StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: colors.background.paper,
|
||
},
|
||
scrollContent: {
|
||
paddingVertical: spacing.lg,
|
||
},
|
||
section: {
|
||
marginBottom: spacing['2xl'],
|
||
maxWidth: CARD_MAX_WIDTH,
|
||
alignSelf: 'center',
|
||
width: '100%',
|
||
paddingHorizontal: spacing['2xl'],
|
||
},
|
||
sectionTitle: {
|
||
fontSize: fontSizes.sm,
|
||
fontWeight: '600',
|
||
color: colors.text.secondary,
|
||
marginBottom: spacing.sm,
|
||
marginTop: spacing.sm,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: 0.5,
|
||
},
|
||
// 聊天预览样式
|
||
previewContainer: {
|
||
backgroundColor: colors.primary.light + '20',
|
||
borderRadius: 14,
|
||
padding: spacing.md,
|
||
minHeight: 140,
|
||
},
|
||
previewMessage: {
|
||
backgroundColor: '#FFFFFF',
|
||
padding: spacing.sm,
|
||
marginBottom: spacing.sm,
|
||
maxWidth: '80%',
|
||
alignSelf: 'flex-start',
|
||
minWidth: 44,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 1 },
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 2,
|
||
elevation: 1,
|
||
},
|
||
previewReply: {
|
||
padding: spacing.sm,
|
||
maxWidth: '80%',
|
||
alignSelf: 'flex-end',
|
||
minWidth: 44,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 1 },
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 2,
|
||
elevation: 1,
|
||
},
|
||
previewText: {
|
||
color: colors.text.primary,
|
||
},
|
||
// 主题选择样式
|
||
themeList: {
|
||
flexDirection: 'row',
|
||
gap: spacing.sm,
|
||
},
|
||
themeItem: {
|
||
width: 70,
|
||
height: 90,
|
||
borderRadius: borderRadius.md,
|
||
borderWidth: 2,
|
||
borderColor: 'transparent',
|
||
overflow: 'hidden',
|
||
padding: spacing.xs,
|
||
},
|
||
themeItemActive: {
|
||
borderColor: colors.primary.main,
|
||
},
|
||
themePreview: {
|
||
flex: 1,
|
||
justifyContent: 'center',
|
||
gap: 4,
|
||
},
|
||
themeBubble1: {
|
||
height: 12,
|
||
borderRadius: 6,
|
||
width: '70%',
|
||
},
|
||
themeBubble2: {
|
||
height: 12,
|
||
borderRadius: 6,
|
||
width: '50%',
|
||
alignSelf: 'flex-end',
|
||
},
|
||
themeIcon: {
|
||
position: 'absolute',
|
||
bottom: 4,
|
||
right: 4,
|
||
width: 18,
|
||
height: 18,
|
||
borderRadius: 9,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
},
|
||
themeName: {
|
||
fontSize: fontSizes.xs,
|
||
textAlign: 'center',
|
||
marginTop: 4,
|
||
color: colors.text.secondary,
|
||
},
|
||
sliderContainer: {
|
||
paddingVertical: 8,
|
||
},
|
||
});
|
||
}
|
||
|
||
export const ChatSettingsScreen: React.FC = () => {
|
||
const colors = useAppColors();
|
||
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);
|
||
|
||
const { setFontSize, setMessageRadius, setTheme } = useChatSettingsActions();
|
||
|
||
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,
|
||
{
|
||
borderRadius: messageRadius,
|
||
borderTopLeftRadius: Math.max(4, messageRadius * 0.3),
|
||
}
|
||
]}>
|
||
<Text style={[styles.previewText, { fontSize }]}>
|
||
早上好!👋
|
||
</Text>
|
||
<Text style={[styles.previewText, { fontSize }]}>
|
||
你知道现在几点吗?
|
||
</Text>
|
||
</View>
|
||
<View style={[
|
||
styles.previewReply,
|
||
{
|
||
borderRadius: messageRadius,
|
||
borderBottomRightRadius: Math.max(4, messageRadius * 0.3),
|
||
backgroundColor: currentTheme.bubble
|
||
}
|
||
]}>
|
||
<Text style={[styles.previewText, { fontSize }]}>
|
||
现在是威海的早晨😎
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
);
|
||
|
||
// 渲染字号滑块
|
||
const renderFontSizeSlider = () => (
|
||
<View style={styles.section}>
|
||
<Text style={styles.sectionTitle}>消息字号</Text>
|
||
<View style={styles.sliderContainer}>
|
||
<Slider
|
||
value={fontSize}
|
||
min={12}
|
||
max={24}
|
||
onValueChange={setFontSize}
|
||
leftLabel="A"
|
||
rightLabel="A"
|
||
/>
|
||
</View>
|
||
</View>
|
||
);
|
||
|
||
// 渲染主题颜色选择
|
||
const renderThemeColors = () => (
|
||
<View style={styles.section}>
|
||
<Text style={styles.sectionTitle}>主题颜色</Text>
|
||
<ScrollView
|
||
horizontal
|
||
showsHorizontalScrollIndicator={false}
|
||
contentContainerStyle={styles.themeList}
|
||
>
|
||
{CHAT_THEMES.map((theme, index) => (
|
||
<TouchableOpacity
|
||
key={theme.id}
|
||
style={[
|
||
styles.themeItem,
|
||
themeIndex === index && styles.themeItemActive,
|
||
{ backgroundColor: theme.secondary },
|
||
]}
|
||
onPress={() => setTheme(index)}
|
||
activeOpacity={0.8}
|
||
>
|
||
<View style={styles.themePreview}>
|
||
<View style={[styles.themeBubble1, { backgroundColor: '#FFFFFF' }]} />
|
||
<View style={[styles.themeBubble2, { backgroundColor: theme.bubble }]} />
|
||
</View>
|
||
<View style={[styles.themeIcon, { backgroundColor: theme.primary }]}>
|
||
<Text style={{ fontSize: 10 }}>{theme.icon}</Text>
|
||
</View>
|
||
</TouchableOpacity>
|
||
))}
|
||
</ScrollView>
|
||
</View>
|
||
);
|
||
|
||
// 渲染圆角滑块
|
||
const renderRadiusSlider = () => (
|
||
<View style={styles.section}>
|
||
<Text style={styles.sectionTitle}>消息圆角</Text>
|
||
<View style={styles.sliderContainer}>
|
||
<Slider
|
||
value={messageRadius}
|
||
min={0}
|
||
max={30}
|
||
onValueChange={setMessageRadius}
|
||
/>
|
||
</View>
|
||
</View>
|
||
);
|
||
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||
<ScrollView
|
||
contentContainerStyle={[
|
||
styles.scrollContent,
|
||
{ paddingHorizontal: responsivePadding, paddingBottom: 80 }
|
||
]}
|
||
>
|
||
{renderFontSizeSlider()}
|
||
{renderChatPreview()}
|
||
{renderThemeColors()}
|
||
{renderRadiusSlider()}
|
||
</ScrollView>
|
||
</SafeAreaView>
|
||
);
|
||
};
|
||
|
||
export default ChatSettingsScreen;
|