feat(service): add foreground service for background message sync

Implement Android foreground service to keep app alive in background for real-time message sync:
- Add withForegroundService expo config plugin for notification channel setup
- Add BackgroundSyncManager with REALTIME, BATTERY_SAVER, and DISABLED modes
- Add ForegroundServiceModule for native Android foreground service binding
- Refactor backgroundService to integrate foreground service and expo-background-task

Also refactor auth and profile screens to use dynamic theme colors:
- Replace hardcoded THEME_COLORS with colors.primary.main, colors.background.paper, etc.
- Add useResolvedColorScheme for dynamic StatusBar styling
- Update NotificationSettingsScreen with sync mode selection UI

BREAKING CHANGE: Switched from expo-background-fetch to expo-background-task, minimum background sync interval changed from 900s to 15min in config
This commit is contained in:
lafay
2026-04-11 04:27:22 +08:00
parent 9bbed8cf5e
commit 6f84e17772
22 changed files with 1271 additions and 394 deletions

View File

@@ -33,7 +33,7 @@ import { useNavigation, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
import { useAppColors } from '../../theme';
import { useAppColors, useResolvedColorScheme } from '../../theme';
import * as hrefs from '../../navigation/hrefs';
import { messageManager, callStore } from '../../stores';
import { useBreakpointGTE } from '../../hooks/useResponsive';
@@ -56,6 +56,7 @@ export const ChatScreen: React.FC = () => {
const router = useRouter();
const insets = useSafeAreaInsets();
const colors = useAppColors();
const resolved = useResolvedColorScheme();
const styles = useChatScreenStyles();
// 响应式布局
@@ -406,7 +407,7 @@ export const ChatScreen: React.FC = () => {
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
>
<StatusBar style="dark" backgroundColor="#FFFFFF" />
<StatusBar style={resolved === 'dark' ? 'light' : 'dark'} backgroundColor={colors.background.paper} />
{/* 顶部栏 */}
<ChatHeader
@@ -499,7 +500,7 @@ export const ChatScreen: React.FC = () => {
>
<View
style={{
backgroundColor: 'rgba(255,255,255,0.9)',
backgroundColor: colors.background.paper + 'E6',
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 6,

View File

@@ -28,7 +28,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useIsFocused } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, borderRadius, useAppColors, type AppColors } from '../../theme';
import { spacing, borderRadius, useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
import { SystemMessageResponse } from '../../types/dto';
import { messageService } from '../../services/messageService';
import { commentService } from '../../services/commentService';
@@ -49,18 +49,12 @@ const MESSAGE_TYPES = [
{ key: 'announcement', title: '公告' },
];
// 胡萝卜橙主题色
const THEME_COLORS = {
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',
};
const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'favorite_post']);
const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']);
export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => {
const colors = useAppColors();
const resolved = useResolvedColorScheme();
const styles = useMemo(() => createNotificationsStyles(colors), [colors]);
const isFocused = useIsFocused();
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
@@ -424,9 +418,9 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
if (!isHydrated) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
</SafeAreaView>
);
@@ -434,7 +428,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
<Animated.View
style={[
styles.content,
@@ -499,7 +493,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
{/* 消息列表 */}
{isLoading && messages.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
) : (
<FlatList
@@ -519,8 +513,8 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[THEME_COLORS.primary]}
tintColor={THEME_COLORS.primary}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
/>
@@ -580,7 +574,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
{/* 消息列表 */}
{isLoading && messages.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
) : (
<FlatList
@@ -600,8 +594,8 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[THEME_COLORS.primary]}
tintColor={THEME_COLORS.primary}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
/>
@@ -617,7 +611,7 @@ function createNotificationsStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
backgroundColor: colors.background.paper,
},
content: {
flex: 1,
@@ -629,7 +623,7 @@ function createNotificationsStyles(colors: AppColors) {
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
backgroundColor: '#fff',
backgroundColor: colors.background.paper,
},
headerWide: {
paddingHorizontal: 32,
@@ -650,7 +644,7 @@ function createNotificationsStyles(colors: AppColors) {
fontSize: 20,
},
unreadBadge: {
backgroundColor: THEME_COLORS.primary,
backgroundColor: colors.primary.main,
borderRadius: 10,
paddingHorizontal: 6,
paddingVertical: 2,
@@ -660,7 +654,7 @@ function createNotificationsStyles(colors: AppColors) {
justifyContent: 'center',
},
unreadBadgeText: {
color: '#fff',
color: colors.text.inverse,
fontSize: 11,
fontWeight: '600',
},
@@ -674,7 +668,7 @@ function createNotificationsStyles(colors: AppColors) {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#F5F5F7',
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
},
@@ -686,7 +680,7 @@ function createNotificationsStyles(colors: AppColors) {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#fff',
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider || '#E5E5EA',
},
@@ -702,7 +696,7 @@ function createNotificationsStyles(colors: AppColors) {
paddingVertical: 8,
marginRight: 8,
borderRadius: 14,
backgroundColor: '#F5F5F7',
backgroundColor: colors.background.default,
borderWidth: 1,
borderColor: 'transparent',
},
@@ -712,15 +706,15 @@ function createNotificationsStyles(colors: AppColors) {
marginRight: 10,
},
filterTagActive: {
backgroundColor: THEME_COLORS.primary,
borderColor: THEME_COLORS.primary,
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
filterTagTextActive: {
fontWeight: '600',
},
filterCount: {
marginLeft: 6,
backgroundColor: THEME_COLORS.primaryLight + '40',
backgroundColor: colors.primary.light + '40',
paddingHorizontal: 6,
paddingVertical: 1,
borderRadius: 8,
@@ -770,7 +764,7 @@ function createNotificationsStyles(colors: AppColors) {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: '#F5F5F7',
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,