diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index eb377cd..2cb6939 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
diff --git a/app.json b/app.json
index 62055b0..ccc2d82 100644
--- a/app.json
+++ b/app.json
@@ -60,7 +60,8 @@
"permissions": [
"android.permission.RECORD_AUDIO",
"android.permission.CAMERA",
- "android.permission.MODIFY_AUDIO_SETTINGS"
+ "android.permission.MODIFY_AUDIO_SETTINGS",
+ "android.permission.POST_NOTIFICATIONS"
]
},
"web": {
diff --git a/functions/src/notifications.js b/functions/src/notifications.js
index 5e80aff..216cc74 100644
--- a/functions/src/notifications.js
+++ b/functions/src/notifications.js
@@ -89,20 +89,26 @@ const sendNotification = async ({
if (!receiver || !message) {
throw new Error("Receiver and message are required");
}
- const { id } = await refList.notifications.add({
+ const payload = {
sender,
receiver,
receiverCollection,
title,
message,
- time: new Date(),
+ time: admin.firestore.FieldValue.serverTimestamp(),
+ read: false,
+ readAt: null,
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;
} catch (e) {
- console.log(e);
+ console.log("[sendNotification] Error creating notification:", e);
}
};
diff --git a/src/components/BaseHeader.js b/src/components/BaseHeader.js
index cd578a7..ce1b7d5 100644
--- a/src/components/BaseHeader.js
+++ b/src/components/BaseHeader.js
@@ -9,9 +9,13 @@ import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import useLayoutType, { sidebarWidth } from "../hooks/useLayoutType.js";
+import useNotifications from "../hooks/useNotifications";
export default ({ title = "", containerStyle = {} }) => {
const { isDesktop } = useLayoutType();
+ const notificationsContext = useNotifications();
+ const unreadCount = notificationsContext?.unreadCount || 0;
+ const hasUnread = unreadCount > 0;
return (
{
}}
/>
-
+ {hasUnread ? (
+
+ ) : null}
diff --git a/src/config/firebase.js b/src/config/firebase.js
index 8825565..780b00c 100644
--- a/src/config/firebase.js
+++ b/src/config/firebase.js
@@ -50,6 +50,7 @@ export const projectsRef = firestore.collection("projects");
export const tasksRef = firestore.collection("tasks");
export const playlistsRef = firestore.collection("playlists");
export const chatsRef = firestore.collection("chats");
+export const notificationsRef = firestore.collection("notifications");
export const { arrayUnion, arrayRemove, increment, serverTimestamp } =
firebase.firestore.FieldValue;
diff --git a/src/config/initialGlobalState.js b/src/config/initialGlobalState.js
index 0290d28..66d499d 100644
--- a/src/config/initialGlobalState.js
+++ b/src/config/initialGlobalState.js
@@ -6,6 +6,7 @@ export default {
currentUserRoles: [],
activeRouteName: null,
+ pendingData: null,
webBackgroundImg: null,
webLayoutMode: "default",
diff --git a/src/hooks/useNotifications.js b/src/hooks/useNotifications.js
new file mode 100644
index 0000000..7e10f67
--- /dev/null
+++ b/src/hooks/useNotifications.js
@@ -0,0 +1,6 @@
+import { useContext } from "react";
+import { NotificationContext } from "../providers/NotificationProvider";
+
+export default function useNotifications() {
+ return useContext(NotificationContext);
+}
diff --git a/src/providers/NotificationProvider.js b/src/providers/NotificationProvider.js
index a91ecf5..6635603 100644
--- a/src/providers/NotificationProvider.js
+++ b/src/providers/NotificationProvider.js
@@ -1,9 +1,21 @@
import Constants from "expo-constants";
import * as Device from "expo-device";
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 { usersRef } from "../config/firebase";
+import firebase, {
+ notificationsRef,
+ serverTimestamp,
+ usersRef,
+} from "../config/firebase";
+import useDataFromRef from "../hooks/useDataFromRef";
export const NotificationContext = createContext(null);
@@ -18,54 +30,140 @@ export default function NotificationProvider({ children }) {
const notificationListener = useRef();
const responseListener = useRef();
- useEffect(() => {
- if (!!uid && !!user && !notifInit) {
- registerForPushNotificationsAsync();
- } else if (notifInit && allowNotifications) {
- notificationListener.current =
- Notifications.addNotificationReceivedListener(async (notification) => {
- console.log("Notification received", notification);
- await Notifications.scheduleNotificationAsync(notification?.request);
- });
+ 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]);
- responseListener.current =
- Notifications.addNotificationResponseReceivedListener(
- async (response) => {
- console.log("Notification response", response);
- if (
- response?.actionIdentifier ===
- "expo.modules.notifications.actions.DEFAULT"
- ) {
- const { data } = response.notification.request.content;
- if (data) {
- console.log("Notification data", data);
- await setPendingData(data);
- }
+ 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(() => {
+ if (!uid || !user || notifInit) {
+ return;
+ }
+ registerForPushNotificationsAsync();
+ }, [user, uid, notifInit]);
+
+ useEffect(() => {
+ if (!allowNotifications) {
+ return;
+ }
+ notificationListener.current =
+ Notifications.addNotificationReceivedListener(async (notification) => {
+ console.log("Notification received", notification);
+ await Notifications.scheduleNotificationAsync(notification?.request);
+ });
+
+ responseListener.current =
+ Notifications.addNotificationResponseReceivedListener(
+ async (response) => {
+ console.log("Notification response", response);
+ if (
+ response?.actionIdentifier ===
+ "expo.modules.notifications.actions.DEFAULT"
+ ) {
+ const { data } = response.notification.request.content;
+ if (data) {
+ console.log("Notification data", data);
+ await setPendingData(data);
}
}
- );
- }
+ }
+ );
return () => {
- if (notifInit && allowNotifications) {
- Notifications.removeNotificationSubscription(
- notificationListener.current
- );
- Notifications.removeNotificationSubscription(responseListener.current);
- }
+ Notifications.removeNotificationSubscription(
+ notificationListener.current
+ );
+ Notifications.removeNotificationSubscription(responseListener.current);
};
- }, [user, uid, notifInit]);
+ }, [allowNotifications, setPendingData]);
async function registerForPushNotificationsAsync() {
try {
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") {
console.log("Notification permissions denied");
+ setAllowNotifications(false);
return;
}
setAllowNotifications(true);
- setNotifInit(true);
// Get the push token
const pushToken = (
@@ -86,6 +184,8 @@ export default function NotificationProvider({ children }) {
}
} catch (e) {
console.log(e);
+ } finally {
+ setNotifInit(true);
}
}
@@ -95,6 +195,12 @@ export default function NotificationProvider({ children }) {
allowNotifications,
pendingData,
setPendingData,
+ notifications,
+ setNotifications,
+ loadingNotifications,
+ unreadCount,
+ markNotificationAsRead,
+ markAllNotificationsAsRead,
}}
>
{children}