Syncing logic improvements... Notification logic refactor (stop spamming notifications on sync)

This commit is contained in:
Milan Paunovic
2024-10-20 13:16:54 +02:00
parent da071736d0
commit 055c40d1e6
5 changed files with 100 additions and 88 deletions

View File

@ -94,7 +94,7 @@ const CalendarSettingsPage = (props: {
newUserData: {googleToken: accessToken, googleMail: googleMail}, newUserData: {googleToken: accessToken, googleMail: googleMail},
}); });
await fetchAndSaveGoogleEvents(accessToken, googleMail) await fetchAndSaveGoogleEvents({token: accessToken, email: googleMail})
} }
} catch (error) { } catch (error) {
console.error("Error during Google sign-in:", error); console.error("Error during Google sign-in:", error);
@ -207,12 +207,12 @@ const CalendarSettingsPage = (props: {
if (appleToken) { if (appleToken) {
console.log("Apple ID token received. Fetch user info if needed..."); console.log("Apple ID token received. Fetch user info if needed...");
// Example: Store user token and email
await updateUserData({ await updateUserData({
newUserData: {appleToken, appleMail}, newUserData: {appleToken, appleMail},
}); });
console.log("User data updated with Apple ID token."); console.log("User data updated with Apple ID token.");
await fetchAndSaveAppleEvents({token: appleToken, email: appleMail!});
} else { } else {
console.warn("Apple authentication was not successful or email was hidden."); console.warn("Apple authentication was not successful or email was hidden.");
} }
@ -252,7 +252,7 @@ const CalendarSettingsPage = (props: {
[] []
); );
const handleChangeFirstDayOfWeek = () => { const handleChangeFirstDayOfWeek = (firstDayOfWeek: string) => {
setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays"); setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
debouncedUpdateFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays"); debouncedUpdateFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
} }
@ -436,12 +436,18 @@ const CalendarSettingsPage = (props: {
<View style={{marginTop: 20}}> <View style={{marginTop: 20}}>
{!!profileData?.googleMail && ( {!!profileData?.googleMail && (
<TouchableOpacity <TouchableOpacity
onPress={() => fetchAndSaveGoogleEvents(undefined, undefined)} onPress={() => fetchAndSaveGoogleEvents({
token: profileData?.googleToken!,
email: profileData?.googleMail!
})}
> >
<View row paddingR-20 center> <View row paddingR-20 center>
<Button <Button
disabled={isSyncingGoogle} disabled={isSyncingGoogle}
onPress={() => fetchAndSaveGoogleEvents(undefined, undefined)} onPress={() => fetchAndSaveGoogleEvents({
token: profileData?.googleToken!,
email: profileData?.googleMail!
})}
label={`Sync ${profileData?.googleMail}`} label={`Sync ${profileData?.googleMail}`}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}} labelProps={{numberOfLines: 3}}
@ -466,11 +472,18 @@ const CalendarSettingsPage = (props: {
{!!profileData?.appleMail && ( {!!profileData?.appleMail && (
<TouchableOpacity> <TouchableOpacity
onPress={() => fetchAndSaveAppleEvents({
email: profileData?.appleMail!,
token: profileData?.appleToken!
})}>
<View row paddingR-20 center> <View row paddingR-20 center>
<Button <Button
disabled={isSyncingApple} disabled={isSyncingApple}
onPress={() => fetchAndSaveAppleEvents(undefined, undefined)} onPress={() => fetchAndSaveAppleEvents({
email: profileData?.appleMail!,
token: profileData?.appleToken!
})}
label={`Sync ${profileData?.appleMail}`} label={`Sync ${profileData?.appleMail}`}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}} labelProps={{numberOfLines: 3}}
@ -494,12 +507,18 @@ const CalendarSettingsPage = (props: {
{!!profileData?.outlookMail && ( {!!profileData?.outlookMail && (
<TouchableOpacity <TouchableOpacity
onPress={() => fetchAndSaveOutlookEvents(undefined, undefined)} onPress={() => fetchAndSaveOutlookEvents({
token: profileData?.microsoftToken!,
email: profileData?.outlookMail!
})}
> >
<View row paddingR-20 center> <View row paddingR-20 center>
<Button <Button
disabled={isSyncingOutlook} disabled={isSyncingOutlook}
onPress={() => fetchAndSaveOutlookEvents(undefined, undefined)} onPress={() => fetchAndSaveOutlookEvents({
token: profileData?.microsoftToken!,
email: profileData?.outlookMail!
})}
label={`Sync ${profileData?.outlookMail}`} label={`Sync ${profileData?.outlookMail}`}
labelStyle={styles.addCalLbl} labelStyle={styles.addCalLbl}
labelProps={{numberOfLines: 3}} labelProps={{numberOfLines: 3}}

View File

@ -9,101 +9,89 @@ const {Expo} = require('expo-server-sdk');
admin.initializeApp(); admin.initializeApp();
const db = admin.firestore(); const db = admin.firestore();
let expo = new Expo({accessToken: process.env.EXPO_ACCESS_TOKEN}); let expo = new Expo({ accessToken: process.env.EXPO_ACCESS_TOKEN });
let notificationTimeout = null;
let eventCount = 0;
let pushTokens = [];
exports.sendNotificationOnEventCreation = functions.firestore exports.sendNotificationOnEventCreation = functions.firestore
.document('Events/{eventId}') .document('Events/{eventId}')
.onCreate(async (snapshot, context) => { .onCreate(async (snapshot, context) => {
const eventData = snapshot.data(); const eventData = snapshot.data();
const { familyId, creatorId } = eventData; const {familyId, creatorId} = eventData;
if (!familyId || !creatorId) { if (!familyId || !creatorId) {
console.error('Missing familyId or creatorId in event data'); console.error('Missing familyId or creatorId in event data');
return; return;
} }
const pushTokens = await getPushTokensForFamilyExcludingCreator(familyId, creatorId);
if (!pushTokens.length) { if (!pushTokens.length) {
console.log('No push tokens available for the event.'); pushTokens = await getPushTokensForFamilyExcludingCreator(familyId, creatorId);
return; if (!pushTokens.length) {
console.log('No push tokens available for the event.');
return;
}
} }
let messages = []; // Increment event count for debouncing
for (let pushToken of pushTokens) { eventCount++;
if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`); if (notificationTimeout) {
continue; clearTimeout(notificationTimeout); // Reset the timer if events keep coming
}
// Set a debounce time (e.g., 5 seconds)
notificationTimeout = setTimeout(async () => {
const eventMessage = eventCount === 1
? `An event "${eventData.title}" has been added. Check it out!`
: `${eventCount} new events have been added.`;
let messages = [];
for (let pushToken of pushTokens) {
if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`);
continue;
}
messages.push({
to: pushToken,
sound: 'default',
title: 'New Events Added!',
body: eventMessage,
data: {eventId: context.params.eventId},
});
} }
messages.push({ let chunks = expo.chunkPushNotifications(messages);
to: pushToken, let tickets = [];
sound: 'default',
title: 'New Event Added!',
body: `An event "${eventData.title}" has been added. Check it out!`,
data: { eventId: context.params.eventId },
});
}
let chunks = expo.chunkPushNotifications(messages); for (let chunk of chunks) {
let tickets = []; try {
for (let chunk of chunks) { let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
try { tickets.push(...ticketChunk);
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...ticketChunk);
for (let ticket of ticketChunk) { for (let ticket of ticketChunk) {
if (ticket.status === 'ok') { if (ticket.status === 'ok') {
console.log('Notification successfully sent:', ticket.id); console.log('Notification successfully sent:', ticket.id);
} else if (ticket.status === 'error') { } else if (ticket.status === 'error') {
console.error(`Notification error: ${ticket.message}`); console.error(`Notification error: ${ticket.message}`);
if (ticket.details && ticket.details.error) { if (ticket.details && ticket.details.error === 'DeviceNotRegistered') {
console.error('Error details:', ticket.details.error);
if (ticket.details.error === 'DeviceNotRegistered') {
console.log(`Removing invalid push token: ${ticket.to}`);
await removeInvalidPushToken(ticket.to); await removeInvalidPushToken(ticket.to);
} }
} }
} }
} catch (error) {
console.error('Error sending notification:', error);
} }
} catch (error) {
console.error('Error sending notification:', error);
} }
}
// Retrieve and handle notification receipts eventCount = 0; // Reset the event count after sending notification
let receiptIds = []; pushTokens = []; // Reset push tokens for the next round
for (let ticket of tickets) {
if (ticket.id) {
receiptIds.push(ticket.id);
}
}
let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds); }, 5000); // Debounce time (5 seconds)
for (let chunk of receiptIdChunks) {
try {
let receipts = await expo.getPushNotificationReceiptsAsync(chunk);
console.log('Receipts:', receipts);
for (let receiptId in receipts) {
let { status, message, details } = receipts[receiptId];
if (status === 'ok') {
console.log(`Notification with receipt ID ${receiptId} was delivered successfully`);
} else if (status === 'error') {
console.error(`Notification error: ${message}`);
if (details && details.error) {
console.error(`Error details: ${details.error}`);
}
}
}
} catch (error) {
console.error('Error retrieving receipts:', error);
}
}
return null;
}); });
exports.createSubUser = onRequest(async (request, response) => { exports.createSubUser = onRequest(async (request, response) => {
const authHeader = request.get('Authorization'); const authHeader = request.get('Authorization');
@ -203,7 +191,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
if (profileData.googleToken) { if (profileData.googleToken) {
try { try {
const refreshedGoogleToken = await refreshGoogleToken(profileData.googleToken); const refreshedGoogleToken = await refreshGoogleToken(profileData.googleToken);
await profileDoc.ref.update({ googleToken: refreshedGoogleToken }); await profileDoc.ref.update({googleToken: refreshedGoogleToken});
console.log(`Google token updated for user ${profileDoc.id}`); console.log(`Google token updated for user ${profileDoc.id}`);
} catch (error) { } catch (error) {
console.error(`Error refreshing Google token for user ${profileDoc.id}:`, error.message); console.error(`Error refreshing Google token for user ${profileDoc.id}:`, error.message);
@ -213,7 +201,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
if (profileData.microsoftToken) { if (profileData.microsoftToken) {
try { try {
const refreshedMicrosoftToken = await refreshMicrosoftToken(profileData.microsoftToken); const refreshedMicrosoftToken = await refreshMicrosoftToken(profileData.microsoftToken);
await profileDoc.ref.update({ microsoftToken: refreshedMicrosoftToken }); await profileDoc.ref.update({microsoftToken: refreshedMicrosoftToken});
console.log(`Microsoft token updated for user ${profileDoc.id}`); console.log(`Microsoft token updated for user ${profileDoc.id}`);
} catch (error) { } catch (error) {
console.error(`Error refreshing Microsoft token for user ${profileDoc.id}:`, error.message); console.error(`Error refreshing Microsoft token for user ${profileDoc.id}:`, error.message);
@ -223,7 +211,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
if (profileData.appleToken) { if (profileData.appleToken) {
try { try {
const refreshedAppleToken = await refreshAppleToken(profileData.appleToken); const refreshedAppleToken = await refreshAppleToken(profileData.appleToken);
await profileDoc.ref.update({ appleToken: refreshedAppleToken }); await profileDoc.ref.update({appleToken: refreshedAppleToken});
console.log(`Apple token updated for user ${profileDoc.id}`); console.log(`Apple token updated for user ${profileDoc.id}`);
} catch (error) { } catch (error) {
console.error(`Error refreshing Apple token for user ${profileDoc.id}:`, error.message); console.error(`Error refreshing Apple token for user ${profileDoc.id}:`, error.message);
@ -289,4 +277,8 @@ async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) {
}); });
return pushTokens; return pushTokens;
}
async function removeInvalidPushToken(pushToken) {
// TODO
} }

View File

@ -9,7 +9,8 @@ export const useFetchAndSaveAppleEvents = () => {
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveAppleEvents"], mutationKey: ["fetchAndSaveAppleEvents"],
mutationFn: async (token?: string, email?: string) => { mutationFn: async ({token, email}: { token?: string, email?: string }) => {
console.log("CALLL")
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5)); const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));
try { try {

View File

@ -9,7 +9,7 @@ export const useFetchAndSaveGoogleEvents = () => {
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveGoogleEvents"], mutationKey: ["fetchAndSaveGoogleEvents"],
mutationFn: async (token?: string, email?: string) => { mutationFn: async ({token, email}: { token?: string; email?: string }) => {
console.log("Fetching Google Calendar events..."); console.log("Fetching Google Calendar events...");
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5)); const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5));

View File

@ -1,15 +1,15 @@
import { useMutation } from "react-query"; import {useMutation} from "react-query";
import { useAuthContext } from "@/contexts/AuthContext"; import {useAuthContext} from "@/contexts/AuthContext";
import { useCreateEventsFromProvider } from "@/hooks/firebase/useCreateEvent"; import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
import { fetchMicrosoftCalendarEvents } from "@/calendar-integration/microsoft-calendar-utils"; import {fetchMicrosoftCalendarEvents} from "@/calendar-integration/microsoft-calendar-utils";
export const useFetchAndSaveOutlookEvents = () => { export const useFetchAndSaveOutlookEvents = () => {
const { profileData } = useAuthContext(); const {profileData} = useAuthContext();
const { mutateAsync: createEventsFromProvider } = useCreateEventsFromProvider(); const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveOutlookEvents"], mutationKey: ["fetchAndSaveOutlookEvents"],
mutationFn: async (token?: string, email?: string) => { mutationFn: async ({token, email}: { token?: string; email?: string }) => {
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3)); const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3));