Files
frontend/src/screens/profile/NotificationSettingsScreen.tsx

259 lines
7.6 KiB
TypeScript
Raw Normal View History

/**
* NotificationSettingsScreen
* BBS -
*
*/
import React, { useState, useEffect, useMemo } from 'react';
import {
View,
StyleSheet,
Switch,
ScrollView,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, borderRadius, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common';
import {
loadNotificationPreferences,
setPushNotificationsPreference,
setSoundPreference,
setVibrationPreference,
} from '../../services/notificationPreferences';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度
const CONTENT_MAX_WIDTH = 720;
interface NotificationSettingItem {
key: string;
title: string;
subtitle?: string;
icon: string;
value: boolean;
onValueChange: (value: boolean) => void;
}
export const NotificationSettingsScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createNotificationSettingsStyles(colors), [colors]);
const [vibrationEnabled, setVibrationEnabledState] = useState(true);
const [pushEnabled, setPushEnabled] = useState(true);
const [soundEnabled, setSoundEnabled] = useState(true);
const { isWideScreen, isMobile } = useResponsive();
const insets = useSafeAreaInsets();
// 底部间距,避免被 TabBar 遮挡
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
// 加载设置(与启动时 hydrate 一致,避免从其它入口进页时 UI 与内存不一致)
useEffect(() => {
const loadSettings = async () => {
try {
const prefs = await loadNotificationPreferences();
setVibrationEnabledState(prefs.vibrationEnabled);
setPushEnabled(prefs.pushEnabled);
setSoundEnabled(prefs.soundEnabled);
} catch (error) {
console.error('加载通知设置失败:', error);
}
};
loadSettings();
}, []);
const toggleVibration = async (value: boolean) => {
try {
setVibrationEnabledState(value);
await setVibrationPreference(value);
} catch (error) {
console.error('保存震动设置失败:', error);
}
};
const togglePush = async (value: boolean) => {
try {
setPushEnabled(value);
await setPushNotificationsPreference(value);
} catch (error) {
console.error('保存推送开关失败:', error);
}
};
const toggleSound = async (value: boolean) => {
try {
setSoundEnabled(value);
await setSoundPreference(value);
} catch (error) {
console.error('保存提示音设置失败:', error);
}
};
const settings: NotificationSettingItem[] = [
{
key: 'push',
title: '接收推送通知',
subtitle: '接收新消息、点赞、评论等推送',
icon: 'bell-outline',
value: pushEnabled,
onValueChange: togglePush,
},
{
key: 'vibration',
title: '消息震动',
subtitle: '收到新消息时震动提醒',
icon: 'vibrate',
value: vibrationEnabled,
onValueChange: toggleVibration,
},
{
key: 'sound',
title: '消息提示音',
subtitle: '收到新消息时播放提示音',
icon: 'volume-high',
value: soundEnabled,
onValueChange: toggleSound,
},
];
// 渲染内容
const renderContent = () => (
<>
{/* 消息通知设置 */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<MaterialCommunityIcons name="message-text-outline" size={18} color={colors.primary.main} />
<Text variant="caption" color={colors.text.secondary} style={styles.sectionTitle}>
</Text>
</View>
<View style={styles.card}>
{settings.map((item, index) => (
<View key={item.key}>
<View style={styles.settingItem}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons
name={item.icon as any}
size={20}
color={colors.primary.main}
/>
</View>
<View style={styles.settingContent}>
<Text variant="body" color={colors.text.primary}>
{item.title}
</Text>
{item.subtitle && (
<Text variant="caption" color={colors.text.secondary} style={styles.subtitle}>
{item.subtitle}
</Text>
)}
</View>
<Switch
value={item.value}
onValueChange={item.onValueChange}
trackColor={{ false: colors.divider, true: colors.primary.main }}
thumbColor={colors.background.paper}
/>
</View>
{index < settings.length - 1 && <View style={styles.divider} />}
</View>
))}
</View>
</View>
{/* 提示信息 */}
<View style={styles.tipContainer}>
<MaterialCommunityIcons name="information-outline" size={16} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.tipText}>
</Text>
</View>
</>
);
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
{renderContent()}
</ScrollView>
</SafeAreaView>
);
};
function createNotificationSettingsStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
paddingVertical: spacing.md,
},
section: {
marginBottom: spacing.lg,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.lg,
marginBottom: spacing.sm,
gap: spacing.xs,
},
sectionTitle: {
fontWeight: '600',
},
card: {
backgroundColor: colors.background.paper,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
overflow: 'hidden',
maxWidth: CONTENT_MAX_WIDTH,
alignSelf: 'center',
width: '100%',
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.md,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
settingContent: {
flex: 1,
},
subtitle: {
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: colors.divider,
marginLeft: 36 + spacing.md + spacing.md,
},
tipContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
marginHorizontal: spacing.lg,
padding: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
gap: spacing.sm,
},
tipText: {
flex: 1,
lineHeight: 20,
},
});
}
export default NotificationSettingsScreen;