frontend: tidy notifications code and rig notifications observer
Parents:
0decf533 file(s) changed
- frontend/app/(tabs)/_layout.tsx +34 -47
- frontend/app/_layout.tsx +37 -16
- frontend/app/notifications.tsx +59 -68
frontend/app/(tabs)/_layout.tsx
1 -
import { Tabs, Redirect, router } from 'expo-router';
2 -
import React, { useEffect } from 'react';
3 -
import { View, ActivityIndicator } from 'react-native';
1 +
import { Tabs, Redirect, router } from "expo-router";
2 +
import React, { useEffect } from "react";
3 +
import { View, ActivityIndicator } from "react-native";
4 4
5 -
import { HapticTab } from '@/components/misc/haptic-tab';
6 -
import { IconSymbol } from '@/components/ui/icon-symbol';
7 -
import { Colors } from '@/constants/theme';
8 -
import { useColorScheme } from '@/hooks/use-color-scheme';
9 -
import { useAuth } from '@/context/AuthContext';
10 -
import * as Notifications from "expo-notifications";
5 +
import { HapticTab } from "@/components/misc/haptic-tab";
6 +
import { IconSymbol } from "@/components/ui/icon-symbol";
7 +
import { Colors } from "@/constants/theme";
8 +
import { useColorScheme } from "@/hooks/use-color-scheme";
9 +
import { useAuth } from "@/context/AuthContext";
11 10
12 11
export default function TabLayout() {
13 12
const colorScheme = useColorScheme();
14 13
const { isAuthenticated, isLoading } = useAuth();
15 14
16 15
useEffect(() => {
17 16
if (!isLoading && !isAuthenticated) {
18 -
router.replace('/auth/login');
17 +
router.replace("/auth/login");
19 18
}
20 19
}, [isAuthenticated, isLoading]);
21 -
22 -
// TODO: Store the notification in a hook ctx, then capture if was
23 -
useEffect(() => {
24 -
const subscription = Notifications.addNotificationResponseReceivedListener(response => {
25 -
const screen = response.notification.request.content.data?.screen;
26 -
27 -
if (screen === 'notifications') {
28 -
router.push('/notifications');
29 -
}
30 -
});
31 -
32 -
// Handle notification tap when app was already open
33 -
Notifications.setNotificationHandler({
34 -
handleNotification: async () => ({
35 -
shouldShowAlert: true,
36 -
shouldPlaySound: false,
37 -
shouldSetBadge: false,
38 -
}),
39 -
});
40 -
41 -
return () => subscription.remove();
42 -
}, []);
43 20
44 21
if (isLoading) {
45 22
return (
46 -
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
23 +
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
47 24
<ActivityIndicator size="large" />
48 25
</View>
49 26
);
50 27
}
51 28
▸ 2 unchanged lines
54 31
}
55 32
56 33
return (
57 34
<Tabs
58 35
screenOptions={{
59 -
tabBarInactiveTintColor: Colors['light'].tabIconDefault,
60 -
tabBarActiveTintColor: Colors['light'].tabIconSelected,
36 +
tabBarInactiveTintColor: Colors["light"].tabIconDefault,
37 +
tabBarActiveTintColor: Colors["light"].tabIconSelected,
61 38
headerShown: false,
62 39
tabBarButton: HapticTab,
63 -
}}>
64 -
40 +
}}
41 +
>
65 42
<Tabs.Screen
66 43
name="history"
67 44
options={{
68 -
title: 'Histórico',
69 -
tabBarIcon: ({ color }) => <IconSymbol size={28} name="history" color={color} />,
45 +
title: "Histórico",
46 +
tabBarIcon: ({ color }) => (
47 +
<IconSymbol size={28} name="history" color={color} />
48 +
),
70 49
}}
71 50
/>
72 51
73 52
<Tabs.Screen
74 53
name="insights"
75 54
options={{
76 -
title: 'Jornada',
77 -
tabBarIcon: ({ color }) => <IconSymbol size={28} name="book" color={color} />,
55 +
title: "Jornada",
56 +
tabBarIcon: ({ color }) => (
57 +
<IconSymbol size={28} name="book" color={color} />
58 +
),
78 59
}}
79 60
/>
80 61
81 62
<Tabs.Screen
82 63
name="new"
83 64
options={{
84 -
title: 'Novo',
85 -
tabBarIcon: ({ color }) => <IconSymbol size={28} name="add-circle" color={color} />,
65 +
title: "Novo",
66 +
tabBarIcon: ({ color }) => (
67 +
<IconSymbol size={28} name="add-circle" color={color} />
68 +
),
86 69
}}
87 70
/>
88 71
89 72
<Tabs.Screen
90 73
name="actions"
91 74
options={{
92 -
title: 'Ações',
93 -
tabBarIcon: ({ color }) => <IconSymbol size={28} name="bolt" color={color} />,
75 +
title: "Ações",
76 +
tabBarIcon: ({ color }) => (
77 +
<IconSymbol size={28} name="bolt" color={color} />
78 +
),
94 79
}}
95 80
/>
96 81
97 82
<Tabs.Screen
98 83
name="settings"
99 84
options={{
100 -
title: 'Ajustes',
101 -
tabBarIcon: ({ color }) => <IconSymbol size={28} name="settings.fill" color={color} />,
85 +
title: "Ajustes",
86 +
tabBarIcon: ({ color }) => (
87 +
<IconSymbol size={28} name="settings.fill" color={color} />
88 +
),
102 89
}}
103 90
/>
104 91
</Tabs>
105 92
);
106 93
}
frontend/app/_layout.tsx
1 -
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
2 -
import { Stack } from 'expo-router';
3 -
import { useColorScheme } from 'react-native';
4 -
import { QueryClientProvider } from '@tanstack/react-query';
5 -
import { queryClient } from '@/lib/queryClient';
1 +
import {
2 +
DarkTheme,
3 +
DefaultTheme,
4 +
ThemeProvider,
5 +
} from "@react-navigation/native";
6 +
import { Stack } from "expo-router";
7 +
import { useColorScheme } from "react-native";
8 +
import { QueryClientProvider } from "@tanstack/react-query";
9 +
import { queryClient } from "@/lib/queryClient";
10 +
import { useNotificationObserver } from "@/hooks/useNotificationObserver";
11 +
import { AuthProvider } from "@/context/AuthContext";
6 12
7 -
import { AuthProvider } from '@/context/AuthContext';
13 +
export const unstable_settings = {
14 +
initialRouteName: "(tabs)",
15 +
};
8 16
9 -
export const unstable_settings = {
10 -
initialRouteName: '(tabs)',
17 +
const AppBootstrap = () => {
18 +
useNotificationObserver();
19 +
return null;
11 20
};
12 21
13 22
export default function RootLayout() {
14 23
const colorScheme = useColorScheme();
15 24
16 25
return (
17 26
<QueryClientProvider client={queryClient}>
18 27
<ThemeProvider value={DefaultTheme}>
19 28
<AuthProvider>
29 +
<AppBootstrap />
20 30
<Stack>
21 31
<Stack.Screen name="index" options={{ headerShown: false }} />
22 32
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
23 33
<Stack.Screen
24 34
name="auth"
25 35
options={{
26 36
headerShown: false,
27 -
presentation: 'modal'
37 +
presentation: "modal",
28 38
}}
29 39
/>
30 40
31 41
<Stack.Screen
32 42
name="profile"
33 43
options={{
34 -
title: 'Editar perfil',
44 +
title: "Editar perfil",
35 45
}}
36 46
/>
37 47
38 48
<Stack.Screen
39 49
name="entry/mood-components"
40 50
options={{
41 -
presentation: 'formSheet',
42 -
title: 'Editar Componentes',
51 +
presentation: "formSheet",
52 +
title: "Editar Componentes",
43 53
sheetAllowedDetents: [0.25, 0.5, 1],
44 -
sheetInitialDetentIndex: 1
54 +
sheetInitialDetentIndex: 1,
45 55
}}
46 56
/>
47 57
48 58
<Stack.Screen
49 59
name="sleep/new"
50 60
options={{
51 -
title: "Registrar Sono"
61 +
title: "Registrar Sono",
52 62
}}
53 63
/>
54 64
55 65
<Stack.Screen
56 66
name="triggers/new"
57 67
options={{
58 -
title: "Registrar Gatilho"
68 +
title: "Registrar Gatilho",
59 69
}}
60 70
/>
61 71
62 -
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
72 +
<Stack.Screen
73 +
name="notifications"
74 +
options={{
75 +
presentation: "modal",
76 +
title: "Notificações",
77 +
}}
78 +
/>
79 +
80 +
<Stack.Screen
81 +
name="modal"
82 +
options={{ presentation: "modal", title: "Modal" }}
83 +
/>
63 84
</Stack>
64 85
</AuthProvider>
65 86
</ThemeProvider>
66 87
</QueryClientProvider>
67 88
);
68 89
}
frontend/app/notifications.tsx
1 1
import {
2 2
View,
3 3
Text,
4 4
StyleSheet,
5 5
ScrollView,
6 6
KeyboardAvoidingView,
7 7
Platform,
8 8
Alert,
9 -
} from 'react-native';
9 +
} from "react-native";
10 10
11 -
import {
12 -
useState, useEffect
13 -
} from 'react';
14 -
import * as Notifications from 'expo-notifications';
11 +
import { useState, useEffect } from "react";
12 +
import * as Notifications from "expo-notifications";
15 13
import { useAuth } from "@/context/AuthContext";
14 +
import { useLocalSearchParams } from "expo-router";
15 +
import { StatusBar } from "expo-status-bar";
16 +
import { SafeAreaView } from "react-native-safe-area-context";
17 +
import { useThemeColor } from "@/hooks/use-theme-color";
18 +
import { Button } from "@/components/ui/Button";
19 +
import { Colors, Spacing } from "@/constants/theme";
20 +
import { Card } from "@/components/ui/Cards";
21 +
import { Section, SectionHeader } from "@/components/ui/Sections";
16 22
17 -
import { StatusBar } from 'expo-status-bar';
18 -
import { SafeAreaView } from 'react-native-safe-area-context';
19 -
import { useThemeColor } from '@/hooks/use-theme-color';
20 -
import { Button } from '@/components/ui/Button';
23 +
type NotificationParams = {
24 +
title?: string;
25 +
body?: string;
26 +
notificationId?: string;
27 +
};
28 +
29 +
Notifications.setNotificationHandler({
30 +
handleNotification: async () => ({
31 +
shouldPlaySound: true,
32 +
shouldSetBadge: false,
33 +
shouldShowBanner: true,
34 +
shouldShowList: true,
35 +
}),
36 +
});
21 37
22 38
export default function NotificationsPage() {
23 39
const { user } = useAuth();
24 -
const backgroundColor = useThemeColor({}, 'background');
25 -
const [expoPushToken, setExpoPushToken] = useState(user.pushToken);
26 -
const [notification, setNotification] = useState<Notifications.Notification | undefined>(
27 -
undefined
28 -
);
29 -
30 -
useEffect(() => {
31 -
const notificationListener = Notifications
32 -
.addNotificationReceivedListener(notification => {
33 -
setNotification(notification);
34 -
});
35 -
36 -
const responseListener = Notifications
37 -
.addNotificationResponseReceivedListener(response => {
38 -
console.log(response);
39 -
});
40 -
41 -
return () => {
42 -
notificationListener.remove();
43 -
responseListener.remove();
44 -
};
45 -
}, []);
40 +
const params = useLocalSearchParams<NotificationParams>();
41 +
const [expoPushToken, setExpoPushToken] = useState(user?.pushToken);
46 42
47 43
useEffect(() => {
48 -
setExpoPushToken(user.pushToken)
44 +
if (user) {
45 +
setExpoPushToken(user.pushToken);
46 +
}
49 47
}, [user]);
50 48
51 -
async function sendPushNotification(expoPushToken: string) {
49 +
async function sendPushNotification() {
52 50
if (expoPushToken) {
53 51
const message = {
54 52
to: expoPushToken,
55 -
sound: 'default',
56 -
title: 'Oi meu querido',
57 -
body: 'Vamo escutar um The Doors',
58 -
data: { screen: 'notifications' },
53 +
sound: "default",
54 +
title: "Oi meu querido",
55 +
body: "Vamo escutar um The Doors",
56 +
data: { screen: "notifications" },
59 57
};
60 58
61 -
const response = await fetch('https://exp.host/--/api/v2/push/send', {
62 -
method: 'POST',
59 +
await fetch("https://exp.host/--/api/v2/push/send", {
60 +
method: "POST",
63 61
headers: {
64 -
Accept: 'application/json',
65 -
'Accept-encoding': 'gzip, deflate',
66 -
'Content-Type': 'application/json',
62 +
Accept: "application/json",
63 +
"Accept-encoding": "gzip, deflate",
64 +
"Content-Type": "application/json",
67 65
},
68 66
body: JSON.stringify(message),
69 67
});
70 68
}
71 69
}
72 70
73 71
return (
74 -
<SafeAreaView style={[styles.container, { backgroundColor }]}>
75 -
<StatusBar style={'dark'} />
76 -
<KeyboardAvoidingView
77 -
behavior='height'
78 -
style={styles.keyboardView}
79 -
>
72 +
<SafeAreaView style={styles.container}>
73 +
<StatusBar style={"dark"} />
74 +
<KeyboardAvoidingView behavior="height" style={styles.keyboardView}>
80 75
<ScrollView
81 76
contentContainerStyle={styles.scrollContainer}
82 77
showsVerticalScrollIndicator={false}
83 78
>
84 -
<View>
85 -
<Text>
86 -
Uma notificação!!!
87 -
</Text>
88 -
</View>
79 +
<Section>
80 +
<SectionHeader title="Recebidas" />
89 81
90 -
<View style={{ alignItems: 'center', justifyContent: 'center' }}>
91 -
<Text>Title: {notification && notification.request.content.title} </Text>
92 -
<Text>Body: {notification && notification.request.content.body}</Text>
93 -
<Text>Data: {notification && JSON.stringify(notification.request.content.data)}</Text>
94 -
</View>
82 +
<Card style={{ padding: Spacing.cardGap }}>
83 +
<Text>Title: {params.title}</Text>
84 +
<Text>Body: {params.body}</Text>
85 +
<Text>
86 +
Data:{" "}
87 +
{params.notificationId && JSON.stringify(params.notificationId)}
88 +
</Text>
89 +
</Card>
90 +
</Section>
95 91
96 -
<Button
97 -
title="Quer ver uma coisa?"
98 -
onPress={async () => {
99 -
await sendPushNotification(expoPushToken);
100 -
}}
101 -
/>
102 -
92 +
<Button title="Quer ver uma coisa?" onPress={sendPushNotification} />
103 93
</ScrollView>
104 94
</KeyboardAvoidingView>
105 95
</SafeAreaView>
106 96
);
107 97
}
108 98
109 99
const styles = StyleSheet.create({
110 100
container: {
111 101
flex: 1,
102 +
backgroundColor: Colors.light.background,
112 103
},
113 104
keyboardView: {
114 105
flex: 1,
115 106
},
116 107
scrollContainer: {
117 108
flexGrow: 1,
118 109
paddingHorizontal: 24,
119 -
justifyContent: 'start',
120 -
minHeight: '100%',
110 +
justifyContent: "start",
111 +
minHeight: "100%",
121 112
},
122 113
header: {
123 -
alignItems: 'left',
114 +
alignItems: "left",
124 115
marginBottom: 40,
125 116
},
126 117
});