eletrotupi / tcc/ commit / 9e31441

frontend: tidy notifications code and rig notifications observer

Pedro Lucas Porcellis porcellis@eletrotupi.com 1 month ago 9e31441a3e93b5fa72bb803819e7bd8e3019a938
Parents: 0decf53
3 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
});