refactor(database): migrate to new modular database layer and unify data access
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m44s
Frontend CI / ota-android (push) Successful in 12m51s
Frontend CI / build-android-apk (push) Successful in 1h1m26s

- 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/
This commit is contained in:
lafay
2026-04-04 08:01:45 +08:00
parent 189b977fac
commit 82c2970a85
76 changed files with 3382 additions and 2000 deletions

View File

@@ -3,17 +3,16 @@
* 胡萝卜BBS - 聊天个性化设置
*/
import React, { useMemo, useCallback } from 'react';
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 { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import {
useAppColors,
spacing,
@@ -28,7 +27,6 @@ import {
useChatSettingsActions,
CHAT_THEMES,
} from '../../stores/chatSettingsStore';
import { useCurrentUser } from '../../stores';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const CARD_MAX_WIDTH = 720;
@@ -43,7 +41,7 @@ interface SliderProps {
showValue?: boolean;
}
// 自定义滑块组件
// 自定义滑块组件(支持拖动)
const Slider: React.FC<SliderProps> = ({
value,
min,
@@ -55,34 +53,51 @@ const Slider: React.FC<SliderProps> = ({
}) => {
const colors = useAppColors();
const styles = useMemo(() => createSliderStyles(colors), [colors]);
const trackWidthRef = useRef(0);
const percentage = ((value - min) / (max - min)) * 100;
const handlePress = useCallback((event: any) => {
const { locationX } = event.nativeEvent;
const sliderWidth = SCREEN_WIDTH - 64; // 考虑边距
const newPercentage = Math.max(0, Math.min(100, (locationX / sliderWidth) * 100));
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}>{value}</Text>}
{showValue && <Text style={styles.value}>{isNaN(value) ? 0 : value}</Text>}
{rightLabel && <Text style={styles.label}>{rightLabel}</Text>}
</View>
<TouchableOpacity
<View
style={styles.trackContainer}
onPress={handlePress}
activeOpacity={1}
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 }]} />
</TouchableOpacity>
</View>
</View>
);
};
@@ -168,29 +183,32 @@ function createStyles(colors: AppColors) {
minHeight: 140,
},
previewMessage: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.md,
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: {
borderRadius: borderRadius.md,
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: {
fontSize: fontSizes.sm,
color: colors.text.primary,
},
previewTime: {
fontSize: fontSizes.xs,
color: colors.text.hint,
marginTop: 2,
alignSelf: 'flex-end',
},
// 主题选择样式
themeList: {
flexDirection: 'row',
@@ -240,106 +258,56 @@ function createStyles(colors: AppColors) {
marginTop: 4,
color: colors.text.secondary,
},
// 设置项样式 - 扁平化
settingItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
},
settingItemLast: {
borderBottomWidth: 0,
},
settingItemLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
iconContainer: {
width: 24,
height: 24,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.lg,
},
settingTitle: {
fontSize: fontSizes.md,
color: colors.text.primary,
},
settingSubtitle: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
marginTop: 2,
},
// 滑块容器 - 扁平化
sliderContainer: {
paddingVertical: 8,
},
// 开关样式
switch: {
width: 50,
height: 28,
borderRadius: 14,
backgroundColor: colors.divider,
padding: 2,
},
switchActive: {
backgroundColor: colors.primary.main,
},
switchThumb: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: colors.background.paper,
},
});
}
export const ChatSettingsScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors();
const styles = useMemo(() => createStyles(colors), [colors]);
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
// 获取当前用户信息
const currentUser = useCurrentUser();
// 从 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 { setFontSize, setMessageRadius, setTheme } = useChatSettingsActions();
const currentTheme = CHAT_THEMES[themeIndex];
// 用户显示名称
const userName = currentUser?.nickname || currentUser?.username || '我';
// 渲染聊天预览
const renderChatPreview = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={[styles.previewContainer, { backgroundColor: currentTheme.secondary }]}>
<View style={[styles.previewMessage, { borderRadius: messageRadius }]}>
<Text style={[styles.previewText, { fontSize }]}>{userName}</Text>
<Text style={[styles.previewText, { fontSize: fontSize - 2 }]}>
<View style={[
styles.previewMessage,
{
borderRadius: messageRadius,
borderTopLeftRadius: Math.max(4, messageRadius * 0.3),
}
]}>
<Text style={[styles.previewText, { fontSize }]}>
👋
</Text>
<Text style={[styles.previewText, { fontSize: fontSize - 2 }]}>
<Text style={[styles.previewText, { fontSize }]}>
</Text>
<Text style={styles.previewTime}>AM12:42</Text>
</View>
<View style={[styles.previewReply, { borderRadius: messageRadius, backgroundColor: currentTheme.bubble }]}>
<View style={[
styles.previewReply,
{
borderRadius: messageRadius,
borderBottomRightRadius: Math.max(4, messageRadius * 0.3),
backgroundColor: currentTheme.bubble
}
]}>
<Text style={[styles.previewText, { fontSize }]}>
😎
</Text>
<Text style={styles.previewTime}>AM12:57</Text>
</View>
</View>
</View>
@@ -410,87 +378,18 @@ export const ChatSettingsScreen: React.FC = () => {
</View>
);
// 渲染其他设置项
const renderOtherSettings = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<TouchableOpacity style={styles.settingItem}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="image-outline" size={22} color={colors.text.secondary} />
</View>
<View>
<Text style={styles.settingTitle}></Text>
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</TouchableOpacity>
<TouchableOpacity style={[styles.settingItem, styles.settingItemLast]}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="palette-outline" size={22} color={colors.text.secondary} />
</View>
<View>
<Text style={styles.settingTitle}></Text>
<Text style={styles.settingSubtitle}>MiMQy</Text>
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</TouchableOpacity>
</View>
);
// 渲染夜间模式开关
const renderNightMode = () => (
<View style={styles.section}>
<View style={[styles.settingItem, styles.settingItemLast]}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="weather-night" size={22} color={colors.text.secondary} />
</View>
<Text style={styles.settingTitle}></Text>
</View>
<TouchableOpacity
style={[styles.switch, nightMode && styles.switchActive]}
onPress={() => toggleNightMode()}
>
<View style={[styles.switchThumb, { alignSelf: nightMode ? 'flex-end' : 'flex-start' }]} />
</TouchableOpacity>
</View>
</View>
);
// 渲染浏览其他主题
const renderBrowseThemes = () => (
<View style={styles.section}>
<TouchableOpacity style={[styles.settingItem, styles.settingItemLast]}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="apps" size={22} color={colors.text.secondary} />
</View>
<Text style={styles.settingTitle}></Text>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</TouchableOpacity>
</View>
);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingHorizontal: responsivePadding }
{ paddingHorizontal: responsivePadding, paddingBottom: 80 }
]}
>
{renderFontSizeSlider()}
{renderChatPreview()}
{renderThemeColors()}
{renderRadiusSlider()}
{renderOtherSettings()}
{renderNightMode()}
{renderBrowseThemes()}
</ScrollView>
</SafeAreaView>
);