All day event package

This commit is contained in:
Milan Paunovic
2024-11-01 22:02:36 +01:00
parent 61fff87975
commit 87137e7b15
12 changed files with 322 additions and 146 deletions

View File

@ -1,15 +1,25 @@
export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) { export async function fetchGoogleCalendarEvents(token, email, familyId, startDate, endDate) {
const response = await fetch( const googleEvents = [];
`https://www.googleapis.com/calendar/v3/calendars/primary/events?single_events=true&time_min=${startDate}&time_max=${endDate}`, let pageToken = null;
{
do {
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
url.searchParams.set('singleEvents', 'true');
url.searchParams.set('timeMin', startDate);
url.searchParams.set('timeMax', endDate);
if (pageToken) url.searchParams.set('pageToken', pageToken);
const response = await fetch(url.toString(), {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
}, });
);
const data = await response.json(); const data = await response.json();
const googleEvents = [];
if (!response.ok) {
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
}
data.items?.forEach((item) => { data.items?.forEach((item) => {
let isAllDay = false; let isAllDay = false;
@ -46,5 +56,9 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
googleEvents.push(googleEvent); googleEvents.push(googleEvent);
}); });
return {googleEvents, success: response.ok}; // Prepare for the next page if it exists
pageToken = data.nextPageToken;
} while (pageToken);
return { googleEvents, success: true };
} }

View File

@ -14,6 +14,7 @@ import { useAtom } from "jotai";
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms"; import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
import { format, isSameDay } from "date-fns"; import { format, isSameDay } from "date-fns";
import { useAuthContext } from "@/contexts/AuthContext"; import { useAuthContext } from "@/contexts/AuthContext";
import {useIsMutating} from "react-query";
export const CalendarHeader = memo(() => { export const CalendarHeader = memo(() => {
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom); const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);

View File

@ -1,6 +1,6 @@
import React, {useCallback, useEffect, useMemo, useState} from "react"; import React, {useCallback, useEffect, useMemo, useState} from "react";
import {Calendar} from "react-native-big-calendar"; import {Calendar} from "react-native-big-calendar";
import {ActivityIndicator, StyleSheet, View, ViewStyle} from "react-native"; import {ActivityIndicator, ScrollView, StyleSheet, View, ViewStyle} from "react-native";
import {useGetEvents} from "@/hooks/firebase/useGetEvents"; import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useAtom, useSetAtom} from "jotai"; import {useAtom, useSetAtom} from "jotai";
import { import {
@ -14,6 +14,9 @@ import {useAuthContext} from "@/contexts/AuthContext";
import {CalendarEvent} from "@/components/pages/calendar/interfaces"; import {CalendarEvent} from "@/components/pages/calendar/interfaces";
import {Text} from "react-native-ui-lib"; import {Text} from "react-native-ui-lib";
import {addDays, compareAsc, isWithinInterval, subDays} from "date-fns"; import {addDays, compareAsc, isWithinInterval, subDays} from "date-fns";
import {useCalSync} from "@/hooks/useCalSync";
import { useIsMutating } from "react-query";
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
interface EventCalendarProps { interface EventCalendarProps {
calendarHeight: number; calendarHeight: number;
@ -37,6 +40,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
const setEventForEdit = useSetAtom(eventForEditAtom); const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom); const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const {isSyncing} = useSyncEvents()
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes()); const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
const todaysDate = new Date(); const todaysDate = new Date();
@ -75,7 +79,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
); );
const memoizedEventCellStyle = useCallback( const memoizedEventCellStyle = useCallback(
(event: CalendarEvent) => ({backgroundColor: event.eventColor}), (event: CalendarEvent) => ({backgroundColor: event.eventColor, fontSize: 14}),
[] []
); );
@ -211,6 +215,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
if (isLoading) { if (isLoading) {
return ( return (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/> <ActivityIndicator size="large" color="#0000ff"/>
</View> </View>
); );
@ -219,6 +224,13 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
// console.log(enrichedEvents, filteredEvents) // console.log(enrichedEvents, filteredEvents)
return ( return (
<>
{isSyncing && (
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
</View>
)}
<Calendar <Calendar
bodyContainerStyle={styles.calHeader} bodyContainerStyle={styles.calHeader}
swipeEnabled swipeEnabled
@ -227,7 +239,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
sortedMonthView sortedMonthView
// enrichedEventsByDate={enrichedEvents} // enrichedEventsByDate={enrichedEvents}
events={filteredEvents} events={filteredEvents}
// eventCellStyle={memoizedEventCellStyle} eventCellStyle={memoizedEventCellStyle}
onPressEvent={handlePressEvent} onPressEvent={handlePressEvent}
weekStartsOn={memoizedWeekStartsOn} weekStartsOn={memoizedWeekStartsOn}
height={calendarHeight} height={calendarHeight}
@ -249,10 +261,10 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
}, },
typography: { typography: {
fontFamily: "PlusJakartaSans_500Medium", fontFamily: "PlusJakartaSans_500Medium",
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 15}, sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
xl: { xl: {
fontFamily: "PlusJakartaSans_500Medium", fontFamily: "PlusJakartaSans_500Medium",
fontSize: 16, fontSize: 14,
}, },
moreLabel: {}, moreLabel: {},
xs: {fontSize: 10}, xs: {fontSize: 10},
@ -261,10 +273,16 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
dayHeaderStyle={dateStyle} dayHeaderStyle={dateStyle}
dayHeaderHighlightColor={"white"} dayHeaderHighlightColor={"white"}
showAdjacentMonths showAdjacentMonths
// headerContainerStyle={mode !== "month" ? {
// overflow:"hidden",
// height: 12,
// } : {}}
hourStyle={styles.hourStyle} hourStyle={styles.hourStyle}
ampm ampm
// renderCustomDateForMonth={renderCustomDateForMonth} // renderCustomDateForMonth={renderCustomDateForMonth}
/> />
</>
); );
} }
); );
@ -291,6 +309,11 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
position: "absolute",
width: "100%",
height: "100%",
zIndex: 100,
backgroundColor: "rgba(255, 255, 255, 0.9)",
}, },
dayHeader: { dayHeader: {
backgroundColor: "#4184f2", backgroundColor: "#4184f2",

View File

@ -181,7 +181,7 @@ exports.generateCustomToken = onRequest(async (request, response) => {
} }
}); });
exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (context) => { exports.refreshTokens = functions.pubsub.schedule('every 45 minutes').onRun(async (context) => {
console.log('Running token refresh job...'); console.log('Running token refresh job...');
const profilesSnapshot = await db.collection('Profiles').get(); const profilesSnapshot = await db.collection('Profiles').get();

View File

@ -20,6 +20,7 @@ export const useGetEvents = () => {
// If family view is active, include family, creator, and attendee events // If family view is active, include family, creator, and attendee events
if (isFamilyView) { if (isFamilyView) {
const familyQuery = db.collection("Events").where("familyID", "==", familyId); const familyQuery = db.collection("Events").where("familyID", "==", familyId);
const creatorQuery = db.collection("Events").where("creatorId", "==", userId); const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId); const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
@ -83,7 +84,7 @@ export const useGetEvents = () => {
const eventColor = profileData?.eventColor || colorMap.pink; const eventColor = profileData?.eventColor || colorMap.pink;
return { return {
id: event.id || Math.random().toString(36).substr(2, 9), // Generate temp ID if missing id: event.id || Math.random().toString(36).slice(2, 9), // Generate temp ID if missing
title: event.title, title: event.title,
start: new Date(event.startDate.seconds * 1000), start: new Date(event.startDate.seconds * 1000),
end: new Date(event.endDate.seconds * 1000), end: new Date(event.endDate.seconds * 1000),
@ -96,5 +97,6 @@ export const useGetEvents = () => {
}, },
staleTime: Infinity, staleTime: Infinity,
cacheTime: Infinity, cacheTime: Infinity,
keepPreviousData: true,
}); });
}; };

View File

@ -46,14 +46,9 @@ export const useCalSync = () => {
const {profileData} = useAuthContext(); const {profileData} = useAuthContext();
const {mutateAsync: updateUserData} = useUpdateUserData(); const {mutateAsync: updateUserData} = useUpdateUserData();
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = useFetchAndSaveGoogleEvents();
useFetchAndSaveGoogleEvents(); const {mutateAsync: fetchAndSaveOutlookEvents, isLoading: isSyncingOutlook} = useFetchAndSaveOutlookEvents();
const { const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} = useFetchAndSaveAppleEvents();
mutateAsync: fetchAndSaveOutlookEvents,
isLoading: isSyncingOutlook,
} = useFetchAndSaveOutlookEvents();
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} =
useFetchAndSaveAppleEvents();
WebBrowser.maybeCompleteAuthSession(); WebBrowser.maybeCompleteAuthSession();
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig); const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
@ -74,6 +69,7 @@ export const useCalSync = () => {
} }
); );
console.log(response)
const userInfo = await userInfoResponse.json(); const userInfo = await userInfoResponse.json();
const googleMail = userInfo.email; const googleMail = userInfo.email;
@ -89,6 +85,7 @@ export const useCalSync = () => {
await fetchAndSaveGoogleEvents({ await fetchAndSaveGoogleEvents({
token: accessToken, token: accessToken,
refreshToken: refreshToken,
email: googleMail, email: googleMail,
}); });
} }
@ -283,6 +280,7 @@ export const useCalSync = () => {
isConnectedToGoogle, isConnectedToGoogle,
isSyncingOutlook, isSyncingOutlook,
isSyncingGoogle, isSyncingGoogle,
isSyncingApple isSyncingApple,
isSyncing: isSyncingApple || isSyncingOutlook || isSyncingGoogle
} }
} }

View File

@ -9,11 +9,12 @@ export const useFetchAndSaveAppleEvents = () => {
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider(); const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveAppleEvents"], mutationKey: ["fetchAndSaveAppleEvents", "sync"],
mutationFn: async ({token, email}: { token?: string, email?: string }) => { mutationFn: async ({token, email, date}: { token?: string; email?: string, date?: Date }) => {
console.log("CALLL") const baseDate = date || new Date();
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); const timeMin = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() - 1));
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 5)); const timeMax = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() + 1));
try { try {
const response = await fetchiPhoneCalendarEvents( const response = await fetchiPhoneCalendarEvents(
profileData?.familyId!, profileData?.familyId!,

View File

@ -3,41 +3,44 @@ import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-
import { useAuthContext } from "@/contexts/AuthContext"; import { useAuthContext } from "@/contexts/AuthContext";
import { useCreateEventsFromProvider } from "@/hooks/firebase/useCreateEvent"; import { useCreateEventsFromProvider } from "@/hooks/firebase/useCreateEvent";
import { useClearTokens } from "@/hooks/firebase/useClearTokens"; import { useClearTokens } from "@/hooks/firebase/useClearTokens";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
export const useFetchAndSaveGoogleEvents = () => { export const useFetchAndSaveGoogleEvents = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient();
const { profileData } = useAuthContext(); const { profileData } = useAuthContext();
const { mutateAsync: createEventsFromProvider } = useCreateEventsFromProvider(); const { mutateAsync: createEventsFromProvider } = useCreateEventsFromProvider();
const { mutateAsync: clearToken } = useClearTokens(); const { mutateAsync: clearToken } = useClearTokens();
const { mutateAsync: updateUserData } = useUpdateUserData();
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveGoogleEvents"], mutationKey: ["fetchAndSaveGoogleEvents", "sync"],
mutationFn: async ({token, email}: { token?: string; email?: string }) => { mutationFn: async ({ token, email, date, refreshToken }: { token?: string; refreshToken?: string; email?: string; date?: Date }) => {
console.log("Fetching Google Calendar events..."); const baseDate = date || new Date();
const timeMin = new Date(new Date().setMonth(new Date().getMonth() - 2)); const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString().slice(0, -5) + "Z";
const timeMax = new Date(new Date().setMonth(new Date().getMonth() + 2)); const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString().slice(0, -5) + "Z";
console.log("Token: ", token); console.log("Token: ", token);
const tryFetchEvents = async (isRetry = false) => {
try { try {
const response = await fetchGoogleCalendarEvents( const response = await fetchGoogleCalendarEvents(
token, token,
email, email,
profileData?.familyId, profileData?.familyId,
timeMin.toISOString().slice(0, -5) + "Z", timeMin,
timeMax.toISOString().slice(0, -5) + "Z" timeMax
); );
if (!response.success) { if (!response.success) {
await clearToken({email: email!, provider: "google"}) await clearToken({ email: email!, provider: "google" });
return return; // Stop refetching if clearing the token
} }
console.log("Google Calendar events fetched:", response); console.log("Google Calendar events fetched:", response);
const items = response?.googleEvents?.map((item) => { const items = response?.googleEvents?.map((item) => {
if (item.allDay) { if (item.allDay) {
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0)); item.startDate = new Date(item.startDate.setHours(0, 0, 0, 0));
item.endDate = item.startDate; item.endDate = item.startDate;
} }
return item; return item;
@ -46,11 +49,59 @@ export const useFetchAndSaveGoogleEvents = () => {
await createEventsFromProvider(items); await createEventsFromProvider(items);
} catch (error) { } catch (error) {
console.error("Error fetching Google Calendar events:", error); console.error("Error fetching Google Calendar events:", error);
throw error; // Ensure errors are propagated to the mutation
if (!isRetry) {
const refreshedToken = await handleRefreshToken(email, refreshToken);
if (refreshedToken) {
await updateUserData({
newUserData: {
googleAccounts: {
...profileData.googleAccounts,
[email!]: { ...profileData.googleAccounts[email!], accessToken: refreshedToken },
},
},
});
return tryFetchEvents(true); // Retry once after refreshing
} else {
await clearToken({ email: email!, provider: "google" });
console.error(`Token refresh failed; token cleared for ${email}`);
throw error;
} }
} else {
console.error(`Retry failed after refreshing token for user ${profileData?.email}:`, error.message);
throw error;
}
}
};
return tryFetchEvents();
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["events"]) queryClient.invalidateQueries(["events"]);
}, },
}); });
}; };
async function handleRefreshToken(email: string, refreshToken: string) {
if (!refreshToken) return null;
try {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
}),
});
const data = await response.json();
return data.access_token;
} catch (error) {
console.error("Error refreshing Google token:", error);
return null;
}
}

View File

@ -9,10 +9,11 @@ export const useFetchAndSaveOutlookEvents = () => {
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider(); const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
return useMutation({ return useMutation({
mutationKey: ["fetchAndSaveOutlookEvents"], mutationKey: ["fetchAndSaveOutlookEvents", "sync"],
mutationFn: async ({token, email}: { token?: string; email?: string }) => { mutationFn: async ({token, email, date}: { token?: string; email?: string, date?: Date }) => {
const timeMin = new Date(new Date().setFullYear(new Date().getFullYear() - 1)); const baseDate = date || new Date();
const timeMax = new Date(new Date().setFullYear(new Date().getFullYear() + 3)); const timeMin = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() - 1));
const timeMax = new Date(new Date(baseDate).setFullYear(new Date(baseDate).getMonth() + 1));
console.log("Token: ", token ?? profileData?.microsoftToken); console.log("Token: ", token ?? profileData?.microsoftToken);

85
hooks/useSyncOnScroll.ts Normal file
View File

@ -0,0 +1,85 @@
import { useState, useEffect, useCallback } from "react";
import { useAuthContext } from "@/contexts/AuthContext";
import { useAtomValue } from "jotai";
import { useFetchAndSaveGoogleEvents } from "./useFetchAndSaveGoogleEvents";
import { useFetchAndSaveAppleEvents } from "./useFetchAndSaveAppleEvents";
import { useFetchAndSaveOutlookEvents } from "./useFetchAndSaveOutlookEvents";
import { selectedDateAtom } from "@/components/pages/calendar/atoms";
import { addDays, subDays, isBefore, isAfter, format } from "date-fns";
export const useSyncEvents = () => {
const { profileData } = useAuthContext();
const selectedDate = useAtomValue(selectedDateAtom);
const [lastSyncDate, setLastSyncDate] = useState<Date>(selectedDate);
const [lowerBoundDate, setLowerBoundDate] = useState<Date>(subDays(selectedDate, 6 * 30));
const [upperBoundDate, setUpperBoundDate] = useState<Date>(addDays(selectedDate, 6 * 30));
const [isSyncing, setIsSyncing] = useState(false);
const [error, setError] = useState(null);
const syncedRanges = useState<Set<string>>(new Set())[0];
const { mutateAsync: fetchAndSaveGoogleEvents } = useFetchAndSaveGoogleEvents();
const { mutateAsync: fetchAndSaveOutlookEvents } = useFetchAndSaveOutlookEvents();
const { mutateAsync: fetchAndSaveAppleEvents } = useFetchAndSaveAppleEvents();
const generateRangeKey = (startDate: Date, endDate: Date) => {
return `${format(startDate, "yyyy-MM-dd")}_${format(endDate, "yyyy-MM-dd")}`;
};
const syncEvents = useCallback(async () => {
setIsSyncing(true);
setError(null);
const newLowerBound = subDays(selectedDate, 6 * 30);
const newUpperBound = addDays(selectedDate, 6 * 30);
const rangeKey = generateRangeKey(newLowerBound, newUpperBound);
if (syncedRanges.has(rangeKey)) {
setIsSyncing(false);
return;
}
if (isBefore(selectedDate, lowerBoundDate) || isAfter(selectedDate, upperBoundDate)) {
try {
const googleEvents = Object.entries(profileData?.googleAccounts || {}).map(([email, { accessToken }]) =>
fetchAndSaveGoogleEvents({ token: accessToken, email, date: selectedDate })
);
const outlookEvents = Object.entries(profileData?.microsoftAccounts || {}).map(([email, token]) =>
fetchAndSaveOutlookEvents({ token, email, date: selectedDate })
);
const appleEvents = Object.entries(profileData?.appleAccounts || {}).map(([email, token]) =>
fetchAndSaveAppleEvents({ token, email, date: selectedDate })
);
await Promise.all([...googleEvents, ...outlookEvents, ...appleEvents]);
setLastSyncDate(selectedDate);
setLowerBoundDate(newLowerBound);
setUpperBoundDate(newUpperBound);
syncedRanges.add(rangeKey);
} catch (err) {
console.error("Error syncing events:", err);
setError(err);
} finally {
setIsSyncing(false);
}
} else {
setIsSyncing(false);
}
}, [selectedDate, lowerBoundDate, upperBoundDate, profileData, fetchAndSaveGoogleEvents, fetchAndSaveOutlookEvents, fetchAndSaveAppleEvents, syncedRanges]);
useEffect(() => {
syncEvents();
}, [selectedDate, syncEvents]);
return {
isSyncing,
error,
lastSyncDate,
lowerBoundDate,
upperBoundDate,
};
};

View File

@ -1,5 +1,5 @@
diff --git a/node_modules/react-native-big-calendar/build/index.js b/node_modules/react-native-big-calendar/build/index.js diff --git a/node_modules/react-native-big-calendar/build/index.js b/node_modules/react-native-big-calendar/build/index.js
index 848ceba..57fbaed 100644 index 848ceba..add1b1c 100644
--- a/node_modules/react-native-big-calendar/build/index.js --- a/node_modules/react-native-big-calendar/build/index.js
+++ b/node_modules/react-native-big-calendar/build/index.js +++ b/node_modules/react-native-big-calendar/build/index.js
@@ -9,6 +9,17 @@ var isoWeek = require('dayjs/plugin/isoWeek'); @@ -9,6 +9,17 @@ var isoWeek = require('dayjs/plugin/isoWeek');