notifications ok

This commit is contained in:
2025-10-07 17:02:12 +02:00
parent a4dca1d4f0
commit c1171ad9e9
8 changed files with 174 additions and 46 deletions
+1
View File
@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+2 -1
View File
@@ -60,7 +60,8 @@
"permissions": [ "permissions": [
"android.permission.RECORD_AUDIO", "android.permission.RECORD_AUDIO",
"android.permission.CAMERA", "android.permission.CAMERA",
"android.permission.MODIFY_AUDIO_SETTINGS" "android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.POST_NOTIFICATIONS"
] ]
}, },
"web": { "web": {
+11 -5
View File
@@ -89,20 +89,26 @@ const sendNotification = async ({
if (!receiver || !message) { if (!receiver || !message) {
throw new Error("Receiver and message are required"); throw new Error("Receiver and message are required");
} }
const { id } = await refList.notifications.add({ const payload = {
sender, sender,
receiver, receiver,
receiverCollection, receiverCollection,
title, title,
message, message,
time: new Date(), time: admin.firestore.FieldValue.serverTimestamp(),
read: false,
readAt: null,
data, data,
}); };
console.log(`Notification ${id} created`); const { id } = await refList.notifications.add(payload);
console.log(
"[sendNotification] Notification created",
JSON.stringify({ id, receiver, receiverCollection }, null, 2)
);
return id; return id;
} catch (e) { } catch (e) {
console.log(e); console.log("[sendNotification] Error creating notification:", e);
} }
}; };
+6
View File
@@ -9,9 +9,13 @@ import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService"; import { navigate } from "../navigation/NavigationService";
import useLayoutType, { sidebarWidth } from "../hooks/useLayoutType.js"; import useLayoutType, { sidebarWidth } from "../hooks/useLayoutType.js";
import useNotifications from "../hooks/useNotifications";
export default ({ title = "", containerStyle = {} }) => { export default ({ title = "", containerStyle = {} }) => {
const { isDesktop } = useLayoutType(); const { isDesktop } = useLayoutType();
const notificationsContext = useNotifications();
const unreadCount = notificationsContext?.unreadCount || 0;
const hasUnread = unreadCount > 0;
return ( return (
<View <View
@@ -49,11 +53,13 @@ export default ({ title = "", containerStyle = {} }) => {
}} }}
/> />
{hasUnread ? (
<View <View
style={{ style={{
...Style.itemBadge, ...Style.itemBadge,
}} }}
/> />
) : null}
</Pressable> </Pressable>
</View> </View>
</View> </View>
+1
View File
@@ -50,6 +50,7 @@ export const projectsRef = firestore.collection("projects");
export const tasksRef = firestore.collection("tasks"); export const tasksRef = firestore.collection("tasks");
export const playlistsRef = firestore.collection("playlists"); export const playlistsRef = firestore.collection("playlists");
export const chatsRef = firestore.collection("chats"); export const chatsRef = firestore.collection("chats");
export const notificationsRef = firestore.collection("notifications");
export const { arrayUnion, arrayRemove, increment, serverTimestamp } = export const { arrayUnion, arrayRemove, increment, serverTimestamp } =
firebase.firestore.FieldValue; firebase.firestore.FieldValue;
+1
View File
@@ -6,6 +6,7 @@ export default {
currentUserRoles: [], currentUserRoles: [],
activeRouteName: null, activeRouteName: null,
pendingData: null,
webBackgroundImg: null, webBackgroundImg: null,
webLayoutMode: "default", webLayoutMode: "default",
+6
View File
@@ -0,0 +1,6 @@
import { useContext } from "react";
import { NotificationContext } from "../providers/NotificationProvider";
export default function useNotifications() {
return useContext(NotificationContext);
}
+116 -10
View File
@@ -1,9 +1,21 @@
import Constants from "expo-constants"; import Constants from "expo-constants";
import * as Device from "expo-device"; import * as Device from "expo-device";
import * as Notifications from "expo-notifications"; import * as Notifications from "expo-notifications";
import React, { createContext, useEffect, useRef, useState } from "react"; import React, {
createContext,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useGlobal } from "reactn"; import { useGlobal } from "reactn";
import { usersRef } from "../config/firebase"; import firebase, {
notificationsRef,
serverTimestamp,
usersRef,
} from "../config/firebase";
import useDataFromRef from "../hooks/useDataFromRef";
export const NotificationContext = createContext(null); export const NotificationContext = createContext(null);
@@ -18,10 +30,95 @@ export default function NotificationProvider({ children }) {
const notificationListener = useRef(); const notificationListener = useRef();
const responseListener = useRef(); const responseListener = useRef();
const notificationsQuery = useMemo(() => {
if (!uid) {
return null;
}
try {
return notificationsRef
.where("receiver", "==", uid)
.orderBy("time", "desc");
} catch (error) {
console.log("notificationsQuery error:", error);
return null;
}
}, [uid]);
const {
data: notifications = [],
loading: loadingNotifications,
setData: setNotifications,
} = useDataFromRef({
ref: notificationsQuery,
initialState: [],
refreshArray: [uid],
listener: true,
condition: !!notificationsQuery,
});
const unreadCount = useMemo(() => {
try {
return notifications.filter((notif) => !notif?.read).length;
} catch (_error) {
return 0;
}
}, [notifications]);
const markNotificationAsRead = useCallback(async (notificationId) => {
try {
if (!notificationId) return;
await notificationsRef.doc(notificationId).set(
{
read: true,
readAt: serverTimestamp(),
},
{ merge: true }
);
} catch (error) {
console.log("markNotificationAsRead error:", error);
}
}, []);
const markAllNotificationsAsRead = useCallback(async () => {
try {
if (!notifications?.length) {
return;
}
const unread = notifications.filter((notif) => !notif?.read);
if (!unread.length) {
return;
}
const batch = firebase.firestore().batch();
unread.forEach((notif) => {
if (!notif?.id) return;
batch.set(
notificationsRef.doc(notif.id),
{
read: true,
readAt: serverTimestamp(),
},
{ merge: true }
);
});
await batch.commit();
} catch (error) {
console.log("markAllNotificationsAsRead error:", error);
}
}, [notifications]);
useEffect(() => { useEffect(() => {
if (!!uid && !!user && !notifInit) { if (!uid || !user || notifInit) {
return;
}
registerForPushNotificationsAsync(); registerForPushNotificationsAsync();
} else if (notifInit && allowNotifications) { }, [user, uid, notifInit]);
useEffect(() => {
if (!allowNotifications) {
return;
}
notificationListener.current = notificationListener.current =
Notifications.addNotificationReceivedListener(async (notification) => { Notifications.addNotificationReceivedListener(async (notification) => {
console.log("Notification received", notification); console.log("Notification received", notification);
@@ -44,28 +141,29 @@ export default function NotificationProvider({ children }) {
} }
} }
); );
}
return () => { return () => {
if (notifInit && allowNotifications) {
Notifications.removeNotificationSubscription( Notifications.removeNotificationSubscription(
notificationListener.current notificationListener.current
); );
Notifications.removeNotificationSubscription(responseListener.current); Notifications.removeNotificationSubscription(responseListener.current);
}
}; };
}, [user, uid, notifInit]); }, [allowNotifications, setPendingData]);
async function registerForPushNotificationsAsync() { async function registerForPushNotificationsAsync() {
try { try {
if (Device.isDevice) { if (Device.isDevice) {
const { status } = await Notifications.requestPermissionsAsync(); let { status } = await Notifications.getPermissionsAsync();
if (status !== "granted") {
const request = await Notifications.requestPermissionsAsync();
status = request.status;
}
if (status !== "granted") { if (status !== "granted") {
console.log("Notification permissions denied"); console.log("Notification permissions denied");
setAllowNotifications(false);
return; return;
} }
setAllowNotifications(true); setAllowNotifications(true);
setNotifInit(true);
// Get the push token // Get the push token
const pushToken = ( const pushToken = (
@@ -86,6 +184,8 @@ export default function NotificationProvider({ children }) {
} }
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} finally {
setNotifInit(true);
} }
} }
@@ -95,6 +195,12 @@ export default function NotificationProvider({ children }) {
allowNotifications, allowNotifications,
pendingData, pendingData,
setPendingData, setPendingData,
notifications,
setNotifications,
loadingNotifications,
unreadCount,
markNotificationAsRead,
markAllNotificationsAsRead,
}} }}
> >
{children} {children}