New calendar

This commit is contained in:
Milan Paunovic
2024-12-15 16:29:34 +01:00
parent a6009beb03
commit 70db8bdc0b
67 changed files with 1568 additions and 1041 deletions

View File

@ -17,7 +17,7 @@
"supportsTablet": true,
"bundleIdentifier": "com.cally.app",
"googleServicesFile": "./ios/GoogleService-Info.plist",
"buildNumber": "74",
"buildNumber": "100",
"usesAppleSignIn": true,
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false

View File

@ -36,6 +36,7 @@ import DrawerIcon from "@/assets/svgs/DrawerIcon";
import { RouteProp } from "@react-navigation/core";
import RefreshButton from "@/components/shared/RefreshButton";
import { useCalSync } from "@/hooks/useCalSync";
import {useIsFetching} from "@tanstack/react-query";
type DrawerParamList = {
index: undefined;
@ -77,6 +78,11 @@ export default function TabLayout() {
const setToDosIndex = useSetAtom(toDosPageIndex);
const { resyncAllCalendars, isSyncing } = useCalSync();
const isFormatting = useIsFetching({queryKey: ['formattedEvents']}) > 0;
const isFetching = useIsFetching({queryKey: ['events']}) > 0;
const isLoading = isSyncing || isFormatting || isFetching;
const onRefresh = React.useCallback(async () => {
try {
await resyncAllCalendars();
@ -116,7 +122,7 @@ export default function TabLayout() {
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
return isCalendarPage ? (
<View marginR-16>
<RefreshButton onRefresh={onRefresh} isSyncing={isSyncing} />
<RefreshButton onRefresh={onRefresh} isSyncing={isLoading} />
</View>
) : null;
}
@ -124,7 +130,7 @@ export default function TabLayout() {
return (
<View marginR-16 row centerV>
{isCalendarPage && (
<View marginR-16><RefreshButton onRefresh={onRefresh} isSyncing={isSyncing} /></View>
<View marginR-16><RefreshButton onRefresh={onRefresh} isSyncing={isLoading} /></View>
)}
<MemoizedViewSwitch navigation={navigation} />
</View>

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, {useEffect} from "react";
import {RefreshControl, ScrollView, View} from "react-native";
import CalendarPage from "@/components/pages/calendar/CalendarPage";
import TabletCalendarPage from "@/components/pages/(tablet_pages)/calendar/TabletCalendarPage";
@ -6,9 +6,9 @@ import * as Device from "expo-device";
import {DeviceType} from "expo-device";
import {useCalSync} from "@/hooks/useCalSync";
import {colorMap} from "@/constants/colorMap";
import { useSetAtom } from "jotai";
import { selectedUserAtom } from "@/components/pages/calendar/atoms";
import { useAuthContext } from "@/contexts/AuthContext";
import {useSetAtom} from "jotai";
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
export default function Screen() {
const isTablet = Device.deviceType === DeviceType.TABLET;
@ -17,9 +17,14 @@ export default function Screen() {
const {profileData} = useAuthContext()
useEffect(() => {
if(!isTablet && profileData) setSelectedUser({firstName: profileData.firstName, lastName: profileData.lastName, eventColor: profileData.eventColor})
if (!isTablet && profileData) setSelectedUser({
uid: profileData.uid!,
firstName: profileData.firstName,
lastName: profileData.lastName,
eventColor: profileData?.eventColor!
})
}, [])
const onRefresh = React.useCallback(async () => {
try {
@ -80,15 +85,8 @@ export default function Screen() {
}
return (
<ScrollView
style={{flex: 1, height: "100%"}}
contentContainerStyle={{flex: 1, height: "100%"}}
bounces={true}
showsVerticalScrollIndicator={false}
>
<View style={{flex: 1}}>
<CalendarPage/>
</View>
</ScrollView>
<View style={{flex: 1}}>
<CalendarPage/>
</View>
);
}

View File

@ -50,19 +50,17 @@ import {Stack} from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import "react-native-reanimated";
import {AuthContextProvider} from "@/contexts/AuthContext";
import {QueryClient, QueryClientProvider} from "react-query";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib";
import {Platform} from 'react-native';
import KeyboardManager from 'react-native-keyboard-manager';
import {enableScreens} from 'react-native-screens';
import {PersistQueryClientProvider} from "@/contexts/PersistQueryClientProvider";
enableScreens(true)
SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient();
if (Platform.OS === 'ios') {
KeyboardManager.setEnable(true);
KeyboardManager.setToolbarPreviousNextButtonEnable(true);
@ -262,7 +260,7 @@ export default function RootLayout() {
}
return (
<QueryClientProvider client={queryClient}>
<PersistQueryClientProvider>
<AuthContextProvider>
<ThemeProvider value={DefaultTheme}>
<Stack>
@ -273,6 +271,6 @@ export default function RootLayout() {
<Toast/>
</ThemeProvider>
</AuthContextProvider>
</QueryClientProvider>
</PersistQueryClientProvider>
);
}

View File

@ -1,8 +1,15 @@
module.exports = function (api) {
api.cache(true);
let plugins = []
if (babelEnv !== 'development') {
plugins.push('transform-remove-console');
}
return {
presets: [
'babel-preset-expo',
]
],
plugins
};
};

View File

@ -0,0 +1,102 @@
import React, {useCallback, useEffect, useMemo, useRef} from "react";
import {useAuthContext} from "@/contexts/AuthContext";
import {useAtomValue} from "jotai";
import {modeAtom, selectedDateAtom, selectedUserAtom} from "@/components/pages/calendar/atoms";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import {CalendarBody, CalendarContainer, CalendarHeader, CalendarKitHandle} from "@howljs/calendar-kit";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useFormattedEvents} from "@/components/pages/calendar/useFormattedEvents";
import {useCalendarControls} from "@/components/pages/calendar/useCalendarControls";
import {EventCell} from "@/components/pages/calendar/EventCell";
import {isToday} from "date-fns";
interface EventCalendarProps {
calendarHeight: number;
calendarWidth: number;
}
export const DetailedCalendar: React.FC<EventCalendarProps> = ({calendarHeight, calendarWidth}) => {
const {profileData} = useAuthContext();
const selectedDate = useAtomValue(selectedDateAtom);
const mode = useAtomValue(modeAtom);
const {data: familyMembers} = useGetFamilyMembers();
const calendarRef = useRef<CalendarKitHandle>(null);
const {data: events} = useGetEvents();
const selectedUser = useAtomValue(selectedUserAtom);
const {data: formattedEvents} = useFormattedEvents(events ?? [], selectedDate, selectedUser);
const {
handlePressEvent,
handlePressCell,
debouncedOnDateChanged
} = useCalendarControls(events ?? []);
const numberOfDays = useMemo(() => {
return mode === 'week' ? 7 : mode === '3days' ? 3 : 1;
}, [mode]);
const firstDay = useMemo(() => {
return profileData?.firstDayOfWeek === "Mondays" ? 1 : 0;
}, [profileData?.firstDayOfWeek]);
const headerProps = useMemo(() => ({
dayBarHeight: 60,
headerBottomHeight: 20
}), []);
const bodyProps = useMemo(() => ({
showNowIndicator: true,
hourFormat: "h:mm a"
}), []);
const containerProps = useMemo(() => ({
hourWidth: 60,
allowPinchToZoom: true,
useHaptic: true,
scrollToNow: true,
initialDate: selectedDate.toISOString(),
}), [selectedDate]);
const renderEvent = useCallback((event: any) => {
const attendees = useMemo(() =>
familyMembers?.filter(member => event?.attendees?.includes(member?.uid!)) || [],
[familyMembers, event.attendees]
);
return (
<EventCell
event={event}
onPress={handlePressEvent}
attendees={attendees}
/>
);
}, [familyMembers, handlePressEvent]);
useEffect(() => {
if (selectedDate && isToday(selectedDate)) {
calendarRef?.current?.goToDate({date: selectedDate});
}
}, [selectedDate]);
return (
<CalendarContainer
ref={calendarRef}
{...containerProps}
numberOfDays={numberOfDays}
calendarWidth={calendarWidth}
onDateChanged={debouncedOnDateChanged}
firstDay={firstDay}
events={formattedEvents ?? []}
onPressEvent={handlePressEvent}
onPressBackground={handlePressCell}
>
<CalendarHeader {...headerProps} />
<CalendarBody
{...bodyProps}
renderEvent={renderEvent}
/>
</CalendarContainer>
);
};
export default DetailedCalendar;

View File

@ -1,734 +1,52 @@
import React, {useCallback, useEffect, useMemo, useState} from "react";
import {Calendar} from "react-native-big-calendar";
import {ActivityIndicator, StyleSheet, TouchableOpacity, View, ViewStyle} from "react-native";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useAtom, useSetAtom} from "jotai";
import React from 'react';
import {StyleSheet, View, ActivityIndicator} from 'react-native';
import {Text} from 'react-native-ui-lib';
import {useGetEvents} from '@/hooks/firebase/useGetEvents';
import {useCalSync} from '@/hooks/useCalSync';
import {useSyncEvents} from '@/hooks/useSyncOnScroll';
import {useAtom} from 'jotai';
import {
editVisibleAtom,
eventForEditAtom,
isAllDayAtom,
isFamilyViewAtom,
modeAtom,
selectedDateAtom,
selectedNewEventDateAtom,
selectedUserAtom,
} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
import {Text} from "react-native-ui-lib";
import {addDays, compareAsc, format, isWithinInterval, subDays} from "date-fns";
import {useCalSync} from "@/hooks/useCalSync";
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
import {colorMap} from "@/constants/colorMap";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import CachedImage from "expo-cached-image";
import { DeviceType } from "expo-device";
import * as Device from "expo-device"
} from './atoms';
import {MonthCalendar} from "@/components/pages/calendar/MonthCalendar";
import {DetailedCalendar} from "@/components/pages/calendar/DetailedCalendar";
interface EventCalendarProps {
calendarHeight: number;
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
calendarWidth: number;
}
const getTotalMinutes = () => {
const date = new Date();
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
};
export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => {
const {data: events, isLoading} = useGetEvents();
const [mode] = useAtom(modeAtom);
const {isSyncing} = useSyncEvents();
useCalSync();
const processEventsForSideBySide = (events: CalendarEvent[]) => {
if (!events) return [];
// Group events by day and time slot
const timeSlots: { [key: string]: CalendarEvent[] } = {};
events.forEach(event => {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
// If it's an all-day event, mark it and add it directly
if (event.allDay) {
const key = `${startDate.toISOString().split('T')[0]}-allday`;
if (!timeSlots[key]) timeSlots[key] = [];
timeSlots[key].push({
...event,
isAllDayEvent: true,
width: 1,
xPos: 0
});
return;
}
// Handle multi-day events
if (startDate.toDateString() !== endDate.toDateString()) {
// Create array of dates between start and end
const dates = [];
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
dates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1);
}
// Create segments for each day
dates.forEach((date, index) => {
const isFirstDay = index === 0;
const isLastDay = index === dates.length - 1;
let segmentStart, segmentEnd;
if (isFirstDay) {
// First day: use original start time to end of day
segmentStart = new Date(startDate);
segmentEnd = new Date(date);
segmentEnd.setHours(23, 59, 59);
} else if (isLastDay) {
// Last day: use start of day to original end time
segmentStart = new Date(date);
segmentStart.setHours(0, 0, 0);
segmentEnd = new Date(endDate);
} else {
// Middle days: full day
segmentStart = new Date(date);
segmentStart.setHours(0, 0, 0);
segmentEnd = new Date(date);
segmentEnd.setHours(23, 59, 59);
}
const key = `${segmentStart.toISOString().split('T')[0]}-${segmentStart.getHours()}`;
if (!timeSlots[key]) timeSlots[key] = [];
timeSlots[key].push({
...event,
start: segmentStart,
end: segmentEnd,
isMultiDaySegment: true,
isFirstDay,
isLastDay,
originalStart: startDate,
originalEnd: endDate,
allDay: true // Mark multi-day events as all-day events
});
});
} else {
// Regular event
const key = `${startDate.toISOString().split('T')[0]}-${startDate.getHours()}`;
if (!timeSlots[key]) timeSlots[key] = [];
timeSlots[key].push(event);
}
});
// Process all time slots
return Object.values(timeSlots).flatMap(slotEvents => {
// Sort events by start time
slotEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
// Find overlapping events (only for non-all-day events)
return slotEvents.map((event, index) => {
// If it's an all-day or multi-day event, return as is
if (event.allDay || event.isMultiDaySegment) {
return {
...event,
width: 1,
xPos: 0
};
}
// Handle regular events
const overlappingEvents = slotEvents.filter((otherEvent, otherIndex) => {
if (index === otherIndex || otherEvent.allDay || otherEvent.isMultiDaySegment) return false;
const eventStart = new Date(event.start);
const eventEnd = new Date(event.end);
const otherStart = new Date(otherEvent.start);
const otherEnd = new Date(otherEvent.end);
return (eventStart < otherEnd && eventEnd > otherStart);
});
const total = overlappingEvents.length + 1;
const position = index % total;
return {
...event,
width: 1 / total,
xPos: position / total
};
});
});
};
const renderEvent = (event: CalendarEvent & {
width: number;
xPos: number;
isMultiDaySegment?: boolean;
isFirstDay?: boolean;
isLastDay?: boolean;
originalStart?: Date;
originalEnd?: Date;
isAllDayEvent?: boolean;
allDay?: boolean;
eventColor?: string;
attendees?: string[];
creatorId?: string;
pfp?: string;
firstName?: string;
lastName?: string;
notes?: string;
hideHours?: boolean;
}, props: any) => {
const {data: familyMembers} = useGetFamilyMembers();
const attendees = useMemo(() => {
if (!familyMembers || !event.attendees) return event?.creatorId ? [event?.creatorId] : [];
return familyMembers.filter(member => event?.attendees?.includes(member?.uid!));
}, [familyMembers, event.attendees]);
if (event.allDay && !!event.isMultiDaySegment) {
if (isLoading || isSyncing) {
return (
<TouchableOpacity
{...props}
style={[
props.style,
{
width: '100%',
flexDirection: 'row',
alignItems: 'center'
}
]}
>
<Text style={styles.allDayEventText} numberOfLines={1}>
{event.title}
{event.isMultiDaySegment &&
` (${format(new Date(event.start), 'MMM d')} - ${format(new Date(event.end), 'MMM d')})`
}
</Text>
</TouchableOpacity>
);
}
const originalStyle = Array.isArray(props.style) ? props.style[0] : props.style;
console.log('Rendering event:', {
title: event.title,
start: event.start,
end: event.end,
width: event.width,
xPos: event.xPos,
isMultiDaySegment: event.isMultiDaySegment
});
// Ensure we have Date objects
const startDate = event.start instanceof Date ? event.start : new Date(event.start);
const endDate = event.end instanceof Date ? event.end : new Date(event.end);
const hourHeight = props.hourHeight || 60;
const startHour = startDate.getHours();
const startMinutes = startDate.getMinutes();
const topPosition = (startHour + startMinutes / 60) * hourHeight;
const endHour = endDate.getHours();
const endMinutes = endDate.getMinutes();
const duration = (endHour + endMinutes / 60) - (startHour + startMinutes / 60);
const height = duration * hourHeight;
const formatTime = (date: Date) => {
const hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? 'pm' : 'am';
const formattedHours = hours % 12 || 12;
const formattedMinutes = minutes.toString().padStart(2, '0');
return `${formattedHours}:${formattedMinutes}${ampm}`;
};
const timeString = event.isMultiDaySegment
? event.isFirstDay
? `${formatTime(startDate)} → 12:00PM`
: event.isLastDay
? `12:00am → ${formatTime(endDate)}`
: 'All day'
: `${formatTime(startDate)} - ${formatTime(endDate)}`;
return (
<TouchableOpacity
{...props}
style={[
originalStyle,
{
position: 'absolute',
width: `${event.width * 100}%`,
left: `${event.xPos * 100}%`,
top: topPosition,
height: height,
zIndex: event.isMultiDaySegment ? 1 : 2,
shadowRadius: 2,
overflow: "hidden"
}
]}
>
<View
style={{
flex: 1,
backgroundColor: event.eventColor,
borderRadius: 4,
padding: 8,
justifyContent: 'space-between'
}}
>
<View>
<Text
style={{
color: 'white',
fontSize: 12,
fontFamily: "PlusJakartaSans_500Medium",
fontWeight: '600',
marginBottom: 4
}}
numberOfLines={1}
>
{event.title}
</Text>
<Text
style={{
color: 'white',
fontSize: 10,
fontFamily: "PlusJakartaSans_500Medium",
opacity: 0.8
}}
>
{timeString}
</Text>
</View>
{/* Attendees Section */}
{attendees?.length > 0 && (
<View style={{flexDirection: 'row', marginTop: 8, height: 27.32}}>
{attendees?.filter(x=>x?.firstName).slice(0, 5).map((attendee, index) => (
<View
key={attendee?.uid}
style={{
position: 'absolute',
left: index * 19,
width: 20,
height: 20,
borderRadius: 50,
borderWidth: 2,
borderColor: '#f2f2f2',
overflow: 'hidden',
backgroundColor: attendee.eventColor || colorMap.pink,
}}
>
{attendee.pfp ? (
<CachedImage
source={{uri: attendee.pfp}}
style={{width: '100%', height: '100%'}}
cacheKey={attendee.pfp}
/>
) : (
<View style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}>
<Text style={{
color: 'white',
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
}}>
{attendee?.firstName?.at(0)}
{attendee?.lastName?.at(0)}
</Text>
</View>
)}
</View>
))}
{attendees.length > 3 && (
<View style={{
position: 'absolute',
left: 3 * 19,
width: 27.32,
height: 27.32,
borderRadius: 50,
borderWidth: 2,
borderColor: '#f2f2f2',
backgroundColor: colorMap.pink,
justifyContent: 'center',
alignItems: 'center'
}}>
<Text style={{
color: 'white',
fontFamily: "Manrope_600SemiBold",
fontSize: 12,
}}>
+{attendees.length - 3}
</Text>
</View>
)}
</View>
)}
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
</View>
</TouchableOpacity>
);
};
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
({calendarHeight}) => {
const {data: events, isLoading} = useGetEvents();
const {profileData, user} = useAuthContext();
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
const [mode, setMode] = useAtom(modeAtom);
const [isFamilyView] = useAtom(isFamilyViewAtom);
//tablet view filter
const [selectedUser] = useAtom(selectedUserAtom);
const setEditVisible = useSetAtom(editVisibleAtom);
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const {isSyncing} = useSyncEvents()
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
useCalSync()
const todaysDate = new Date();
const handlePressEvent = useCallback(
(event: CalendarEvent) => {
if (mode === "day" || mode === "week" || mode === "3days") {
setEditVisible(true);
setEventForEdit(event);
} else {
setMode("day");
setSelectedDate(event.start);
}
},
[setEditVisible, setEventForEdit, mode]
);
const handlePressCell = useCallback(
(date: Date) => {
if (mode === "day" || mode === "week" || mode === "3days") {
setSelectedNewEndDate(date);
} else {
setMode("day");
setSelectedDate(date);
}
},
[mode, setSelectedNewEndDate, setSelectedDate]
);
const handlePressDayHeader = useCallback(
(date: Date) => {
if (mode === "day") {
setIsAllDay(true);
setSelectedNewEndDate(date);
setEditVisible(true);
}
if (mode === 'week' || mode === '3days') {
setSelectedDate(date)
setMode("day")
}
},
[mode, setSelectedNewEndDate]
);
const handleSwipeEnd = useCallback(
(date: Date) => {
setSelectedDate(date);
},
[setSelectedDate]
);
const memoizedEventCellStyle = useCallback(
(event: CalendarEvent) => {
let eventColor = event.eventColor;
if (!isFamilyView && (event.attendees?.includes(user?.uid!) || event.creatorId! === user?.uid)) {
eventColor = profileData?.eventColor ?? colorMap.teal;
}
return {backgroundColor: eventColor, fontSize: 14}
},
[]
);
const memoizedWeekStartsOn = useMemo(
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
[profileData]
);
const isSameDate = useCallback((date1: Date, date2: Date) => {
return (
date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear()
);
}, []);
const dayHeaderColor = useMemo(() => {
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
}, [selectedDate, mode]);
const dateStyle = useMemo(() => {
if (mode === "week" || mode === "3days") return undefined;
return isSameDate(todaysDate, selectedDate) && mode === "day"
? styles.dayHeader
: styles.otherDayHeader;
}, [selectedDate, mode]);
const memoizedHeaderContentStyle = useMemo(() => {
if (mode === "day") {
return styles.dayModeHeader;
} else if (mode === "week" || mode === "3days") {
return styles.weekModeHeader;
} else if (mode === "month") {
return styles.monthModeHeader;
} else {
return {};
}
}, [mode]);
const {enrichedEvents, filteredEvents} = useMemo(() => {
const startTime = Date.now();
const startOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
const endOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
let eventsToFilter = events;
if (selectedUser && Device.deviceType === DeviceType.TABLET) {
eventsToFilter = events?.filter(event =>
event.attendees?.includes(selectedUser.uid) ||
event.creatorId === selectedUser.uid
);
}
const filteredEvents =
eventsToFilter?.filter(
(event) =>
event.start &&
event.end &&
isWithinInterval(event.start, {
start: subDays(selectedDate, startOffset),
end: addDays(selectedDate, endOffset),
}) &&
isWithinInterval(event.end, {
start: subDays(selectedDate, startOffset),
end: addDays(selectedDate, endOffset),
})
) ?? [];
const enrichedEvents = filteredEvents.reduce((acc, event) => {
const dateKey = event.start.toISOString().split("T")[0];
acc[dateKey] = acc[dateKey] || [];
acc[dateKey].push({
...event,
overlapPosition: false,
overlapCount: 0,
});
acc[dateKey].sort((a: { start: any; }, b: { start: any; }) => compareAsc(a.start, b.start));
return acc;
}, {} as Record<string, CalendarEvent[]>);
const endTime = Date.now();
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
return {enrichedEvents, filteredEvents};
}, [events, selectedDate, mode, selectedUser]);
const renderCustomDateForMonth = (date: Date) => {
const circleStyle = useMemo<ViewStyle>(
() => ({
width: 30,
height: 30,
justifyContent: "center",
alignItems: "center",
borderRadius: 15,
}),
[]
);
const defaultStyle = useMemo<ViewStyle>(
() => ({
...circleStyle,
}),
[circleStyle]
);
const currentDateStyle = useMemo<ViewStyle>(
() => ({
...circleStyle,
backgroundColor: "#4184f2",
}),
[circleStyle]
);
const renderDate = useCallback(
(date: Date) => {
const isCurrentDate = isSameDate(todaysDate, date);
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
return (
<View style={{alignItems: "center"}}>
<View style={appliedStyle}>
<Text style={{color: isCurrentDate ? "white" : "black"}}>
{date.getDate()}
</Text>
</View>
</View>
);
},
[todaysDate, currentDateStyle, defaultStyle]
);
return renderDate(date);
};
const processedEvents = useMemo(() => {
return processEventsForSideBySide(filteredEvents);
}, [filteredEvents]);
useEffect(() => {
setOffsetMinutes(getTotalMinutes());
}, [events, mode]);
if (isLoading) {
return (
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
</View>
);
}
return (
<>
{isSyncing && (
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
</View>
)}
<Calendar
bodyContainerStyle={styles.calHeader}
swipeEnabled
mode={mode}
sortedMonthView
events={filteredEvents}
// renderEvent={renderEvent}
eventCellStyle={memoizedEventCellStyle}
allDayEventCellStyle={memoizedEventCellStyle}
// enableEnrichedEvents={true}
// enrichedEventsByDate={enrichedEvents}
onPressEvent={handlePressEvent}
weekStartsOn={memoizedWeekStartsOn}
height={calendarHeight}
activeDate={todaysDate}
date={selectedDate}
onPressCell={handlePressCell}
headerContentStyle={memoizedHeaderContentStyle}
onSwipeEnd={handleSwipeEnd}
scrollOffsetMinutes={offsetMinutes}
theme={{
palette: {
nowIndicator: profileData?.eventColor || "#fd1575",
gray: {
"100": "#e8eaed",
"200": "#e8eaed",
"500": "#b7b7b7",
"800": "#919191",
},
},
typography: {
fontFamily: "PlusJakartaSans_500Medium",
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
xl: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 14,
},
moreLabel: {},
xs: {fontSize: 10},
},
}}
dayHeaderStyle={dateStyle}
dayHeaderHighlightColor={"white"}
showAdjacentMonths
headerContainerStyle={mode !== "month" ? {
overflow: "hidden",
} : {}}
hourStyle={styles.hourStyle}
onPressDateHeader={handlePressDayHeader}
ampm
// renderCustomDateForMonth={renderCustomDateForMonth}
/>
<View style={{backgroundColor: 'white', height: 50, width: '100%'}}/>
</>
);
}
);
return mode === "month"
? <MonthCalendar {...props} />
: <DetailedCalendar {...props} />;
});
const styles = StyleSheet.create({
segmentslblStyle: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
calHeader: {
borderWidth: 0,
paddingBottom: 60,
},
dayModeHeader: {
alignSelf: "flex-start",
justifyContent: "space-between",
alignContent: "center",
width: 38,
right: 42,
height: 13,
},
weekModeHeader: {},
monthModeHeader: {},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
position: "absolute",
width: "100%",
height: "100%",
zIndex: 100,
backgroundColor: "rgba(255, 255, 255, 0.9)",
},
dayHeader: {
backgroundColor: "#4184f2",
aspectRatio: 1,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
},
otherDayHeader: {
backgroundColor: "transparent",
color: "#919191",
aspectRatio: 1,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
},
hourStyle: {
color: "#5f6368",
fontSize: 12,
fontFamily: "Manrope_500Medium",
},
eventCell: {
flex: 1,
borderRadius: 4,
padding: 4,
height: '100%',
justifyContent: 'center',
},
eventTitle: {
color: 'white',
fontSize: 12,
fontFamily: "PlusJakartaSans_500Medium",
alignItems: 'center',
position: 'absolute',
width: '100%',
height: '100%',
zIndex: 100,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
},
});
export default EventCalendar;

View File

@ -0,0 +1,117 @@
import React from 'react';
import {StyleSheet, TouchableOpacity, View} from 'react-native';
import {Text} from 'react-native-ui-lib';
import CachedImage from 'expo-cached-image';
import {format} from 'date-fns';
import {colorMap} from '@/constants/colorMap';
interface EventCellProps {
event: any;
onPress: (event: any) => void;
attendees: any[];
}
export const EventCell: React.FC<EventCellProps> = React.memo((
{
event,
onPress,
attendees
}) => {
return (
<TouchableOpacity
onPress={() => onPress(event)}
style={[styles.eventCell, {backgroundColor: event.eventColor}]}
>
<Text style={styles.eventTitle} numberOfLines={1}>
{event.title}
</Text>
<Text style={[styles.eventTitle, {fontSize: 10, opacity: 0.8}]}>
{format(new Date(event.start.dateTime), 'h:mm a')} - {format(new Date(event.end.dateTime), 'h:mm a')}
</Text>
{attendees.length > 0 && (
<View style={styles.attendeesContainer}>
{attendees.slice(0, 3).map((attendee, index) => (
<View
key={attendee?.uid}
style={[styles.attendeeIcon, {left: index * 19}]}
>
{attendee.pfp ? (
<CachedImage
source={{uri: attendee.pfp}}
style={styles.attendeeImage}
cacheKey={attendee.pfp}
/>
) : (
<Text style={styles.attendeeInitials}>
{attendee?.firstName?.at(0)}
{attendee?.lastName?.at(0)}
</Text>
)}
</View>
))}
{attendees.length > 3 && (
<View style={[styles.attendeeCount, {left: 3 * 19}]}>
<Text style={styles.attendeeCountText}>
+{attendees.length - 3}
</Text>
</View>
)}
</View>
)}
</TouchableOpacity>
);
});
const styles = StyleSheet.create({
eventCell: {
flex: 1,
borderRadius: 4,
padding: 8,
height: '100%',
},
eventTitle: {
color: 'white',
fontSize: 12,
fontFamily: "PlusJakartaSans_500Medium",
},
attendeesContainer: {
flexDirection: 'row',
marginTop: 8,
height: 27.32,
},
attendeeIcon: {
position: 'absolute',
width: 20,
height: 20,
borderRadius: 50,
borderWidth: 2,
borderColor: '#f2f2f2',
overflow: 'hidden',
},
attendeeImage: {
width: '100%',
height: '100%',
},
attendeeInitials: {
color: 'white',
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
attendeeCount: {
position: 'absolute',
width: 27.32,
height: 27.32,
borderRadius: 50,
borderWidth: 2,
borderColor: '#f2f2f2',
backgroundColor: colorMap.pink,
justifyContent: 'center',
alignItems: 'center',
},
attendeeCountText: {
color: 'white',
fontFamily: "Manrope_600SemiBold",
fontSize: 12,
},
});

View File

@ -0,0 +1,723 @@
import React, {useCallback, useEffect, useMemo, useState} from "react";
import {Calendar} from "react-native-big-calendar";
import {ActivityIndicator, StyleSheet, TouchableOpacity, View, ViewStyle} from "react-native";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useAtom, useSetAtom} from "jotai";
import {
editVisibleAtom,
eventForEditAtom,
isAllDayAtom,
isFamilyViewAtom,
modeAtom,
selectedDateAtom,
selectedNewEventDateAtom,
selectedUserAtom,
} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
import {Text} from "react-native-ui-lib";
import {addDays, compareAsc, format, isWithinInterval, subDays} from "date-fns";
import {useCalSync} from "@/hooks/useCalSync";
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
import {colorMap} from "@/constants/colorMap";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import CachedImage from "expo-cached-image";
import { DeviceType } from "expo-device";
import * as Device from "expo-device"
interface EventCalendarProps {
calendarHeight: number;
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
calendarWidth: number;
}
const getTotalMinutes = () => {
const date = new Date();
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
};
const processEventsForSideBySide = (events: CalendarEvent[]) => {
if (!events) return [];
const timeSlots: { [key: string]: CalendarEvent[] } = {};
events.forEach(event => {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
// If it's an all-day event, mark it and add it directly
if (event.allDay) {
const key = `${startDate.toISOString().split('T')[0]}-allday`;
if (!timeSlots[key]) timeSlots[key] = [];
timeSlots[key].push({
...event,
isAllDayEvent: true,
allDay: true,
width: 1,
xPos: 0
});
return;
}
// Handle multi-day events
if (startDate.toDateString() !== endDate.toDateString()) {
// Create array of dates between start and end
const dates = [];
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
dates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1);
}
// Create segments for each day
dates.forEach((date, index) => {
const isFirstDay = index === 0;
const isLastDay = index === dates.length - 1;
let segmentStart, segmentEnd;
if (isFirstDay) {
// First day: use original start time to end of day
segmentStart = new Date(startDate);
segmentEnd = new Date(date);
segmentEnd.setHours(23, 59, 59);
} else if (isLastDay) {
// Last day: use start of day to original end time
segmentStart = new Date(date);
segmentStart.setHours(0, 0, 0);
segmentEnd = new Date(endDate);
} else {
// Middle days: full day
segmentStart = new Date(date);
segmentStart.setHours(0, 0, 0);
segmentEnd = new Date(date);
segmentEnd.setHours(23, 59, 59);
}
const key = `${segmentStart.toISOString().split('T')[0]}-${segmentStart.getHours()}`;
if (!timeSlots[key]) timeSlots[key] = [];
timeSlots[key].push({
...event,
start: segmentStart,
end: segmentEnd,
isMultiDaySegment: true,
isFirstDay,
isLastDay,
originalStart: startDate,
originalEnd: endDate,
allDay: true // Mark multi-day events as all-day events
});
});
} else {
// Regular event
const key = `${startDate.toISOString().split('T')[0]}-${startDate.getHours()}`;
if (!timeSlots[key]) timeSlots[key] = [];
timeSlots[key].push(event);
}
});
// Process all time slots
return Object.values(timeSlots).flatMap(slotEvents => {
// Sort events by start time
slotEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
// Find overlapping events (only for non-all-day events)
return slotEvents.map((event, index) => {
// If it's an all-day or multi-day event, return as is
if (event.allDay || event.isMultiDaySegment) {
return {
...event,
width: 1,
xPos: 0
};
}
// Handle regular events
const overlappingEvents = slotEvents.filter((otherEvent, otherIndex) => {
if (index === otherIndex || otherEvent.allDay || otherEvent.isMultiDaySegment) return false;
const eventStart = new Date(event.start);
const eventEnd = new Date(event.end);
const otherStart = new Date(otherEvent.start);
const otherEnd = new Date(otherEvent.end);
return (eventStart < otherEnd && eventEnd > otherStart);
});
const total = overlappingEvents.length + 1;
const position = index % total;
return {
...event,
width: 1 / total,
xPos: position / total
};
});
});
};
const renderEvent = (event: CalendarEvent & {
width: number;
xPos: number;
isMultiDaySegment?: boolean;
isFirstDay?: boolean;
isLastDay?: boolean;
originalStart?: Date;
originalEnd?: Date;
isAllDayEvent?: boolean;
allDay?: boolean;
eventColor?: string;
attendees?: string[];
creatorId?: string;
pfp?: string;
firstName?: string;
lastName?: string;
notes?: string;
hideHours?: boolean;
}, props: any) => {
const {data: familyMembers} = useGetFamilyMembers();
const attendees = useMemo(() => {
if (!familyMembers || !event.attendees) return event?.creatorId ? [event?.creatorId] : [];
return familyMembers.filter(member => event?.attendees?.includes(member?.uid!));
}, [familyMembers, event.attendees]);
if (event.allDay && !!event.isMultiDaySegment) {
return (
<TouchableOpacity
{...props}
style={[
props.style,
{
width: '100%',
flexDirection: 'row',
alignItems: 'center'
}
]}
>
<Text style={styles.allDayEventText} numberOfLines={1}>
{event.title}
{event.isMultiDaySegment &&
` (${format(new Date(event.start), 'MMM d')} - ${format(new Date(event.end), 'MMM d')})`
}
</Text>
</TouchableOpacity>
);
}
const originalStyle = Array.isArray(props.style) ? props.style[0] : props.style;
const startDate = event.start instanceof Date ? event.start : new Date(event.start);
const endDate = event.end instanceof Date ? event.end : new Date(event.end);
const hourHeight = props.hourHeight || 60;
const startHour = startDate.getHours();
const startMinutes = startDate.getMinutes();
const topPosition = (startHour + startMinutes / 60) * hourHeight;
const endHour = endDate.getHours();
const endMinutes = endDate.getMinutes();
const duration = (endHour + endMinutes / 60) - (startHour + startMinutes / 60);
const height = duration * hourHeight;
const formatTime = (date: Date) => {
const hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? 'pm' : 'am';
const formattedHours = hours % 12 || 12;
const formattedMinutes = minutes.toString().padStart(2, '0');
return `${formattedHours}:${formattedMinutes}${ampm}`;
};
const timeString = event.isMultiDaySegment
? event.isFirstDay
? `${formatTime(startDate)} → 12:00PM`
: event.isLastDay
? `12:00am → ${formatTime(endDate)}`
: 'All day'
: `${formatTime(startDate)} - ${formatTime(endDate)}`;
return (
<TouchableOpacity
{...props}
style={[
originalStyle,
{
position: 'absolute',
width: `${event.width * 100}%`,
left: `${event.xPos * 100}%`,
top: topPosition,
height: height,
zIndex: event.isMultiDaySegment ? 1 : 2,
shadowRadius: 2,
overflow: "hidden"
}
]}
>
<View
style={{
flex: 1,
backgroundColor: event.eventColor,
borderRadius: 4,
padding: 8,
justifyContent: 'space-between'
}}
>
<View>
<Text
style={{
color: 'white',
fontSize: 12,
fontFamily: "PlusJakartaSans_500Medium",
fontWeight: '600',
marginBottom: 4
}}
numberOfLines={1}
>
{event.title}
</Text>
<Text
style={{
color: 'white',
fontSize: 10,
fontFamily: "PlusJakartaSans_500Medium",
opacity: 0.8
}}
>
{timeString}
</Text>
</View>
{/* Attendees Section */}
{attendees?.length > 0 && (
<View style={{flexDirection: 'row', marginTop: 8, height: 27.32}}>
{attendees?.filter(x=>x?.firstName).slice(0, 5).map((attendee, index) => (
<View
key={attendee?.uid}
style={{
position: 'absolute',
left: index * 19,
width: 20,
height: 20,
borderRadius: 50,
borderWidth: 2,
borderColor: '#f2f2f2',
overflow: 'hidden',
backgroundColor: attendee.eventColor || colorMap.pink,
}}
>
{attendee.pfp ? (
<CachedImage
source={{uri: attendee.pfp}}
style={{width: '100%', height: '100%'}}
cacheKey={attendee.pfp}
/>
) : (
<View style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}>
<Text style={{
color: 'white',
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
}}>
{attendee?.firstName?.at(0)}
{attendee?.lastName?.at(0)}
</Text>
</View>
)}
</View>
))}
{attendees.length > 3 && (
<View style={{
position: 'absolute',
left: 3 * 19,
width: 27.32,
height: 27.32,
borderRadius: 50,
borderWidth: 2,
borderColor: '#f2f2f2',
backgroundColor: colorMap.pink,
justifyContent: 'center',
alignItems: 'center'
}}>
<Text style={{
color: 'white',
fontFamily: "Manrope_600SemiBold",
fontSize: 12,
}}>
+{attendees.length - 3}
</Text>
</View>
)}
</View>
)}
</View>
</TouchableOpacity>
);
};
export const MonthCalendar: React.FC<EventCalendarProps> = React.memo(
({calendarHeight}) => {
const {data: events, isLoading} = useGetEvents();
const {profileData, user} = useAuthContext();
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
const [mode, setMode] = useAtom(modeAtom);
const [isFamilyView] = useAtom(isFamilyViewAtom);
//tablet view filter
const [selectedUser] = useAtom(selectedUserAtom);
const setEditVisible = useSetAtom(editVisibleAtom);
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const {isSyncing} = useSyncEvents()
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
useCalSync()
const todaysDate = new Date();
const handlePressEvent = useCallback(
(event: CalendarEvent) => {
if (mode === "day" || mode === "week" || mode === "3days") {
setEditVisible(true);
setEventForEdit(event);
} else {
setMode("day");
setSelectedDate(event.start);
}
},
[setEditVisible, setEventForEdit, mode]
);
const handlePressCell = useCallback(
(date: Date) => {
if (mode === "day" || mode === "week" || mode === "3days") {
setSelectedNewEndDate(date);
} else {
setMode("day");
setSelectedDate(date);
}
},
[mode, setSelectedNewEndDate, setSelectedDate]
);
const handlePressDayHeader = useCallback(
(date: Date) => {
if (mode === "day") {
setIsAllDay(true);
setSelectedNewEndDate(date);
setEditVisible(true);
}
if (mode === 'week' || mode === '3days') {
setSelectedDate(date)
setMode("day")
}
},
[mode, setSelectedNewEndDate]
);
const handleSwipeEnd = useCallback(
(date: Date) => {
setSelectedDate(date);
},
[setSelectedDate]
);
const memoizedEventCellStyle = useCallback(
(event: CalendarEvent) => {
let eventColor = event.eventColor;
if (!isFamilyView && (event.attendees?.includes(user?.uid!) || event.creatorId! === user?.uid)) {
eventColor = profileData?.eventColor ?? colorMap.teal;
}
return {backgroundColor: eventColor, fontSize: 14}
},
[]
);
const memoizedWeekStartsOn = useMemo(
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
[profileData]
);
const isSameDate = useCallback((date1: Date, date2: Date) => {
return (
date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear()
);
}, []);
const dayHeaderColor = useMemo(() => {
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
}, [selectedDate, mode]);
const dateStyle = useMemo(() => {
if (mode === "week" || mode === "3days") return undefined;
return isSameDate(todaysDate, selectedDate) && mode === "day"
? styles.dayHeader
: styles.otherDayHeader;
}, [selectedDate, mode]);
const memoizedHeaderContentStyle = useMemo(() => {
if (mode === "day") {
return styles.dayModeHeader;
} else if (mode === "week" || mode === "3days") {
return styles.weekModeHeader;
} else if (mode === "month") {
return styles.monthModeHeader;
} else {
return {};
}
}, [mode]);
const {enrichedEvents, filteredEvents} = useMemo(() => {
const startTime = Date.now();
const startOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
const endOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
let eventsToFilter = events;
if (selectedUser && Device.deviceType === DeviceType.TABLET) {
eventsToFilter = events?.filter(event =>
event.attendees?.includes(selectedUser.uid) ||
event.creatorId === selectedUser.uid
);
}
const filteredEvents =
eventsToFilter?.filter(
(event) =>
event.start &&
event.end &&
isWithinInterval(event.start, {
start: subDays(selectedDate, startOffset),
end: addDays(selectedDate, endOffset),
}) &&
isWithinInterval(event.end, {
start: subDays(selectedDate, startOffset),
end: addDays(selectedDate, endOffset),
})
) ?? [];
const enrichedEvents = filteredEvents.reduce((acc, event) => {
const dateKey = event.start.toISOString().split("T")[0];
acc[dateKey] = acc[dateKey] || [];
acc[dateKey].push({
...event,
overlapPosition: false,
overlapCount: 0,
});
acc[dateKey].sort((a: { start: any; }, b: { start: any; }) => compareAsc(a.start, b.start));
return acc;
}, {} as Record<string, CalendarEvent[]>);
const endTime = Date.now();
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
return {enrichedEvents, filteredEvents};
}, [events, selectedDate, mode, selectedUser]);
const renderCustomDateForMonth = (date: Date) => {
const circleStyle = useMemo<ViewStyle>(
() => ({
width: 30,
height: 30,
justifyContent: "center",
alignItems: "center",
borderRadius: 15,
}),
[]
);
const defaultStyle = useMemo<ViewStyle>(
() => ({
...circleStyle,
}),
[circleStyle]
);
const currentDateStyle = useMemo<ViewStyle>(
() => ({
...circleStyle,
backgroundColor: "#4184f2",
}),
[circleStyle]
);
const renderDate = useCallback(
(date: Date) => {
const isCurrentDate = isSameDate(todaysDate, date);
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
return (
<View style={{alignItems: "center"}}>
<View style={appliedStyle}>
<Text style={{color: isCurrentDate ? "white" : "black"}}>
{date.getDate()}
</Text>
</View>
</View>
);
},
[todaysDate, currentDateStyle, defaultStyle]
);
return renderDate(date);
};
const processedEvents = useMemo(() => {
return processEventsForSideBySide(filteredEvents);
}, [filteredEvents]);
useEffect(() => {
setOffsetMinutes(getTotalMinutes());
}, [events, mode]);
if (isLoading) {
return (
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
</View>
);
}
return (
<>
{isSyncing && (
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
</View>
)}
<Calendar
bodyContainerStyle={styles.calHeader}
swipeEnabled
mode={mode}
sortedMonthView
events={filteredEvents}
// renderEvent={renderEvent}
eventCellStyle={memoizedEventCellStyle}
allDayEventCellStyle={memoizedEventCellStyle}
// enableEnrichedEvents={true}
// enrichedEventsByDate={enrichedEvents}
onPressEvent={handlePressEvent}
weekStartsOn={memoizedWeekStartsOn}
height={calendarHeight}
activeDate={todaysDate}
date={selectedDate}
onPressCell={handlePressCell}
headerContentStyle={memoizedHeaderContentStyle}
onSwipeEnd={handleSwipeEnd}
scrollOffsetMinutes={offsetMinutes}
theme={{
palette: {
nowIndicator: profileData?.eventColor || "#fd1575",
gray: {
"100": "#e8eaed",
"200": "#e8eaed",
"500": "#b7b7b7",
"800": "#919191",
},
},
typography: {
fontFamily: "PlusJakartaSans_500Medium",
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
xl: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 14,
},
moreLabel: {},
xs: {fontSize: 10},
},
}}
dayHeaderStyle={dateStyle}
dayHeaderHighlightColor={"white"}
showAdjacentMonths
headerContainerStyle={mode !== "month" ? {
overflow: "hidden",
} : {}}
hourStyle={styles.hourStyle}
onPressDateHeader={handlePressDayHeader}
ampm
// renderCustomDateForMonth={renderCustomDateForMonth}
/>
<View style={{backgroundColor: 'white', height: 50, width: '100%'}}/>
</>
);
}
);
const styles = StyleSheet.create({
segmentslblStyle: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},
calHeader: {
borderWidth: 0,
paddingBottom: 60,
},
dayModeHeader: {
alignSelf: "flex-start",
justifyContent: "space-between",
alignContent: "center",
width: 38,
right: 42,
height: 13,
},
weekModeHeader: {},
monthModeHeader: {},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
position: "absolute",
width: "100%",
height: "100%",
zIndex: 100,
backgroundColor: "rgba(255, 255, 255, 0.9)",
},
dayHeader: {
backgroundColor: "#4184f2",
aspectRatio: 1,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
},
otherDayHeader: {
backgroundColor: "transparent",
color: "#919191",
aspectRatio: 1,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
},
hourStyle: {
color: "#5f6368",
fontSize: 12,
fontFamily: "Manrope_500Medium",
},
eventCell: {
flex: 1,
borderRadius: 4,
padding: 4,
height: '100%',
justifyContent: 'center',
},
eventTitle: {
color: 'white',
fontSize: 12,
fontFamily: "PlusJakartaSans_500Medium",
},
});

View File

@ -1 +0,0 @@

View File

@ -0,0 +1,45 @@
import { useCallback } from 'react';
import debounce from 'debounce';
import { useSetAtom } from 'jotai';
import {
editVisibleAtom,
eventForEditAtom,
selectedNewEventDateAtom,
selectedDateAtom,
} from '@/components/pages/calendar/atoms';
import {DateOrDateTime} from "@howljs/calendar-kit";
export const useCalendarControls = (events: any[]) => {
const setEditVisible = useSetAtom(editVisibleAtom);
const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const setSelectedDate = useSetAtom(selectedDateAtom);
const handlePressEvent = useCallback((event: any) => {
const foundEvent = events?.find(x => x.id === event.id);
setEditVisible(true);
setEventForEdit(foundEvent!);
}, [events, setEditVisible, setEventForEdit]);
const handlePressCell = useCallback((date: DateOrDateTime) => {
const selectedDate = new Date(date.dateTime!);
const minutes = selectedDate.getMinutes();
selectedDate.setMinutes(minutes - (minutes % 30), 0, 0); // Also sets seconds and milliseconds to 0
setSelectedNewEndDate(selectedDate);
}, [setSelectedNewEndDate]);
const debouncedOnDateChanged = useCallback(
debounce((date: string) => {
setSelectedDate(new Date(date));
}, 50),
[setSelectedDate]
);
return {
handlePressEvent,
handlePressCell,
debouncedOnDateChanged
};
};

View File

@ -0,0 +1,154 @@
import { DeviceType } from 'expo-device';
import * as Device from 'expo-device';
import { format } from 'date-fns';
import { useQuery } from "@tanstack/react-query";
interface EventTimestamp {
seconds: number;
nanoseconds: number;
}
interface Event {
id: string;
title: string;
start: Date | EventTimestamp;
startDate?: EventTimestamp;
end: Date | EventTimestamp;
endDate?: EventTimestamp;
allDay: boolean;
eventColor: string;
attendees?: string[];
creatorId?: string;
}
interface FormattedEvent {
id: string;
title: string;
start: { date: string } | { dateTime: string; timeZone: string };
end: { date: string } | { dateTime: string; timeZone: string };
color: string;
}
// Precompute time constants
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const PERIOD_IN_MS = 45 * DAY_IN_MS;
const TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Memoize date range calculations
const getDateRangeKey = (timestamp: number) => Math.floor(timestamp / PERIOD_IN_MS);
// Optimized date validation and conversion
const getValidDate = (date: any, timestamp?: EventTimestamp): Date | null => {
try {
if (timestamp?.seconds) {
return new Date(timestamp.seconds * 1000);
}
if (date instanceof Date) {
return date;
}
if (typeof date === 'string') {
return new Date(date);
}
return null;
} catch {
return null;
}
};
// Batch process events
const processEvents = async (
events: Event[],
selectedDate: Date,
selectedUser: { uid: string } | null
): Promise<FormattedEvent[]> => {
// Early return if no events
if (!events.length) return [];
// Pre-calculate constants
const currentRangeKey = getDateRangeKey(selectedDate.getTime());
const isTablet = Device.deviceType === DeviceType.TABLET;
const userId = selectedUser?.uid;
// Process in chunks to avoid blocking the main thread
const CHUNK_SIZE = 100;
const results: FormattedEvent[] = [];
for (let i = 0; i < events.length; i += CHUNK_SIZE) {
const chunk = events.slice(i, i + CHUNK_SIZE);
// Process chunk and await to give UI thread a chance to breathe
await new Promise(resolve => setTimeout(resolve, 0));
for (const event of chunk) {
try {
// Quick user filter
if (isTablet && userId &&
!event.attendees?.includes(userId) &&
event.creatorId !== userId) {
continue;
}
// Validate dates first
const startDate = getValidDate(event.start, event.startDate);
if (!startDate) continue;
const rangeKey = getDateRangeKey(startDate.getTime());
// Skip events outside our range
if (Math.abs(rangeKey - currentRangeKey) > 1) continue;
const endDate = getValidDate(event.end, event.endDate);
if (!endDate) continue;
if (event.allDay) {
const dateStr = format(startDate, 'yyyy-MM-dd');
const endDateStr = format(endDate, 'yyyy-MM-dd');
results.push({
id: event.id,
title: event.title,
start: { date: dateStr },
end: { date: endDateStr },
color: event.eventColor
});
} else {
results.push({
id: event.id,
title: event.title,
start: {
dateTime: startDate.toISOString(),
timeZone: TIME_ZONE
},
end: {
dateTime: endDate.toISOString(),
timeZone: TIME_ZONE
},
color: event.eventColor
});
}
} catch (error) {
console.error('Error processing event:', event.id, error);
continue;
}
}
}
return results;
};
export const useFormattedEvents = (
events: Event[],
selectedDate: Date,
selectedUser: { uid: string } | null
) => {
return useQuery({
queryKey: ['formattedEvents', events, selectedDate, selectedUser?.uid],
queryFn: () => processEvents(events, selectedDate, selectedUser),
enabled: events.length > 0,
staleTime: Infinity,
placeholderData: (previousData) => previousData,
gcTime: Infinity
});
};

View File

@ -9,7 +9,7 @@ import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import {Platform} from 'react-native';
import {useQueryClient} from "react-query";
import {useQueryClient} from "@tanstack/react-query";
export enum ProfileType {

View File

@ -0,0 +1,77 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useState, useEffect } from 'react';
import type { AsyncPersistRetryer } from '@tanstack/query-async-storage-persister';
const createQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 2,
},
},
});
export function PersistQueryClientProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => createQueryClient());
const [isRestored, setIsRestored] = useState(false);
useEffect(() => {
const initPersistence = async () => {
const retry: AsyncPersistRetryer = async ({ persistedClient, error, errorCount }) => {
if (errorCount < 3) return persistedClient;
return undefined;
};
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'REACT_QUERY_CACHE',
throttleTime: 1000,
serialize: data => JSON.stringify(data),
deserialize: str => JSON.parse(str),
retry
});
try {
persistQueryClient({
queryClient,
persister: asyncStoragePersister,
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
buster: 'v1',
dehydrateOptions: {
shouldDehydrateQuery: query => {
const persistedQueries = ['events'];
return persistedQueries.some(key =>
Array.isArray(query.queryKey) &&
query.queryKey.includes(key)
);
},
},
});
setIsRestored(true);
} catch (error) {
console.error('Failed to restore query cache:', error);
setIsRestored(true);
}
};
initPersistence();
return () => {
queryClient.clear();
};
}, [queryClient]);
if (!isRestored) {
return null;
}
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}

View File

@ -1144,12 +1144,14 @@ exports.cleanupTokenRefreshFlags = functions.pubsub
async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, creatorId}) {
const baseDate = new Date();
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
const oneYearAgo = new Date(baseDate);
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
const oneYearAhead = new Date(baseDate);
oneYearAhead.setFullYear(oneYearAhead.getFullYear() + 1);
let totalEvents = 0;
let pageToken = null;
const batchSize = 50;
const batchSize = 250;
try {
console.log(`[FETCH] Starting event fetch for user: ${email}`);
@ -1158,12 +1160,12 @@ async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, c
let events = [];
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/primary/events`);
url.searchParams.set("singleEvents", "true");
url.searchParams.set("timeMin", timeMin);
url.searchParams.set("timeMax", timeMax);
url.searchParams.set("timeMin", oneYearAgo.toISOString());
url.searchParams.set("timeMax", oneYearAhead.toISOString());
url.searchParams.set("maxResults", batchSize.toString());
if (pageToken) url.searchParams.set("pageToken", pageToken);
console.log(`[FETCH] Making request with token: ${token.substring(0, 10)}...`);
console.log(`[FETCH] Making request with token: ${token?.substring(0, 10)}...`);
let response = await fetch(url.toString(), {
headers: {
@ -1178,12 +1180,10 @@ async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, c
console.log(`[TOKEN] Token refreshed successfully during fetch`);
token = refreshedGoogleToken;
// Update token in Firestore
await db.collection("Profiles").doc(creatorId).update({
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
});
// Retry the request with new token
response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${refreshedGoogleToken}`,

View File

@ -280,6 +280,13 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
"@react-native-async-storage/async-storage@1.23.1":
version "1.23.1"
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz#cad3cd4fab7dacfe9838dce6ecb352f79150c883"
integrity sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==
dependencies:
merge-options "^3.0.4"
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@ -1470,6 +1477,11 @@ is-path-inside@^3.0.3:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
@ -1688,6 +1700,13 @@ merge-descriptors@1.0.3:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
merge-options@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7"
integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==
dependencies:
is-plain-obj "^2.1.0"
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"

View File

@ -1,4 +1,4 @@
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import storage from "@react-native-firebase/storage";
import {useAuthContext} from "@/contexts/AuthContext";

View File

@ -1,4 +1,4 @@
import {useMutation} from "react-query";
import {useMutation} from "@tanstack/react-query";
import {UserProfile} from "@firebase/auth";
import {useAuthContext} from "@/contexts/AuthContext";
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";

View File

@ -1,5 +1,5 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import {EventData} from "@/hooks/firebase/types/eventData";

View File

@ -1,5 +1,5 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import { IFeedback } from "@/contexts/FeedbackContext";

View File

@ -1,4 +1,4 @@
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import { useAuthContext } from "@/contexts/AuthContext";
import {IGrocery} from "@/hooks/firebase/types/groceryData";

View File

@ -1,5 +1,5 @@
import { useAuthContext } from "@/contexts/AuthContext";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import { IFeedback } from "@/contexts/FeedbackContext";
import { IBrainDump } from "@/contexts/DumpContext";

View File

@ -1,4 +1,4 @@
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import functions from '@react-native-firebase/functions';
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";

View File

@ -1,4 +1,4 @@
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import { useAuthContext } from "@/contexts/AuthContext";
import { DAYS_OF_WEEK_ENUM, IToDo, REPEAT_TYPE } from "@/hooks/firebase/types/todoData";

View File

@ -1,4 +1,4 @@
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
export const useDeleteEvent = () => {

View File

@ -1,5 +1,5 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
export const useDeleteFeedback = () => {

View File

@ -1,4 +1,4 @@
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
export const useDeleteGrocery = () => {

View File

@ -1,5 +1,5 @@
import { useAuthContext } from "@/contexts/AuthContext";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
export const useDeleteNote = () => {

View File

@ -1,4 +1,4 @@
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {useAuthContext} from "@/contexts/AuthContext";
import firestore from "@react-native-firebase/firestore";
import {Notification} from "@/hooks/firebase/useGetNotifications";

View File

@ -1,5 +1,5 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation} from "react-query";
import {useMutation} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import auth, {FirebaseAuthTypes} from "@react-native-firebase/auth";

View File

@ -1,5 +1,5 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation} from "react-query";
import {useMutation} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import {FirebaseAuthTypes} from "@react-native-firebase/auth";

View File

@ -1,4 +1,4 @@
import { useMutation } from "react-query";
import { useMutation } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
export const useSignUp = () => {

View File

@ -1,4 +1,4 @@
import {useQuery} from "react-query";
import {useQuery} from "@tanstack/react-query";
import {ChildProfile} from "@/hooks/firebase/types/profileTypes";
import firestore from "@react-native-firebase/firestore";
import {useAuthContext} from "@/contexts/AuthContext";

View File

@ -1,11 +1,10 @@
import {useQuery, useQueryClient} from "react-query";
import {useQuery, useQueryClient} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import {useAuthContext} from "@/contexts/AuthContext";
import {useAtomValue} from "jotai";
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
import {colorMap} from "@/constants/colorMap";
import {uuidv4} from "@firebase/util";
import {useEffect} from "react";
import {useEffect, useRef} from "react";
const createEventHash = (event: any): string => {
const str = `${event.startDate?.seconds || ''}-${event.endDate?.seconds || ''}-${
@ -21,206 +20,120 @@ const createEventHash = (event: any): string => {
return hash.toString(36);
};
export const useGetEvents = () => {
const {user, profileData} = useAuthContext();
const isFamilyView = useAtomValue(isFamilyViewAtom);
const queryClient = useQueryClient();
const lastSyncTimestamp = useRef<number>(0);
useEffect(() => {
if (!profileData?.familyId) {
console.log('[SYNC] No family ID available, skipping listener setup');
return;
}
console.log('[SYNC] Setting up sync listener', {
familyId: profileData.familyId,
userId: user?.uid,
isFamilyView
});
if (!profileData?.familyId) return;
const unsubscribe = firestore()
.collection('Households')
.where("familyId", "==", profileData.familyId)
.onSnapshot((snapshot) => {
console.log('[SYNC] Snapshot received', {
empty: snapshot.empty,
size: snapshot.size,
changes: snapshot.docChanges().length
});
snapshot.docChanges().forEach((change) => {
console.log('[SYNC] Processing change', {
type: change.type,
docId: change.doc.id,
newData: change.doc.data()
});
if (change.type === 'modified') {
const data = change.doc.data();
console.log('[SYNC] Modified document data', {
hasLastSyncTimestamp: !!data?.lastSyncTimestamp,
hasLastUpdateTimestamp: !!data?.lastUpdateTimestamp,
allFields: Object.keys(data || {})
});
if (data?.lastSyncTimestamp) {
console.log('[SYNC] Sync timestamp change detected', {
timestamp: data.lastSyncTimestamp.toDate(),
householdId: change.doc.id,
queryKey: ["events", user?.uid, isFamilyView]
});
console.log('[SYNC] Invalidating queries...');
queryClient.invalidateQueries(["events", user?.uid, isFamilyView]);
console.log('[SYNC] Queries invalidated');
} else {
console.log('[SYNC] Modified document without lastSyncTimestamp', {
householdId: change.doc.id
});
const newTimestamp = data.lastSyncTimestamp.seconds;
if (newTimestamp > lastSyncTimestamp.current) {
lastSyncTimestamp.current = newTimestamp;
queryClient.invalidateQueries(["events", user?.uid, isFamilyView]);
}
}
}
});
}, (error) => {
console.error('[SYNC] Listener error:', {
message: error.message,
code: error.code,
stack: error.stack
});
});
}, console.error);
console.log('[SYNC] Listener setup complete');
return () => {
console.log('[SYNC] Cleaning up sync listener', {
familyId: profileData.familyId,
userId: user?.uid
});
unsubscribe();
};
return unsubscribe;
}, [profileData?.familyId, user?.uid, isFamilyView, queryClient]);
return useQuery({
queryKey: ["events", user?.uid, isFamilyView],
queryFn: async () => {
console.log(`Fetching events - Family View: ${isFamilyView}, User: ${user?.uid}`);
const db = firestore();
const userId = user?.uid;
const familyId = profileData?.familyId;
let allEvents = [];
const eventsQuery = db.collection("Events");
let constraints = [];
if (isFamilyView) {
const [publicFamilyEvents, privateCreatorEvents, privateAttendeeEvents, userAttendeeEvents] = await Promise.all([
// Public family events
db.collection("Events")
constraints = [
eventsQuery
.where("familyId", "==", familyId)
.where("private", "==", false)
.get(),
// Private events user created
db.collection("Events")
.where("familyId", "==", familyId)
.where("private", "==", true)
.where("creatorId", "==", userId)
.get(),
// Private events user is attending
db.collection("Events")
.where("private", "==", true)
.where("private", "==", false),
eventsQuery
.where("creatorId", "==", userId),
eventsQuery
.where("attendees", "array-contains", userId)
.get(),
// All events where user is attendee
db.collection("Events")
.where("attendees", "array-contains", userId)
.get(),
]);
allEvents = [
...publicFamilyEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
...privateCreatorEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
...privateAttendeeEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
...userAttendeeEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
];
} else {
const [creatorEvents, attendeeEvents] = await Promise.all([
db.collection("Events")
.where("creatorId", "==", userId)
.get(),
db.collection("Events")
.where("attendees", "array-contains", userId)
.get()
]);
console.log(`Found ${creatorEvents.size} creator events, ${attendeeEvents.size} attendee events`);
allEvents = [
...creatorEvents.docs.map(doc => ({...doc.data(), id: doc.id})),
...attendeeEvents.docs.map(doc => ({...doc.data(), id: doc.id}))
constraints = [
eventsQuery.where("creatorId", "==", userId),
eventsQuery.where("attendees", "array-contains", userId)
];
}
const uniqueEventsMap = new Map();
const processedHashes = new Set();
allEvents.forEach(event => {
const eventHash = createEventHash(event);
console.log(`Processing ${uniqueEventsMap.size} unique events`);
const processedEvent = {
...event,
id: event.id || uuidv4(),
creatorId: event.creatorId || userId
};
// Only add the event if we haven't seen this hash before
if (!processedHashes.has(eventHash)) {
processedHashes.add(eventHash);
uniqueEventsMap.set(processedEvent.id, processedEvent);
} else {
console.log(`Duplicate event detected and skipped using hash: ${eventHash}`);
}
});
console.log(`Processing ${uniqueEventsMap.size} unique events after deduplication`);
const processedEvents = await Promise.all(
Array.from(uniqueEventsMap.values()).map(async (event) => {
const profileSnapshot = await db
.collection("Profiles")
.doc(event.creatorId)
.get();
const profileData = profileSnapshot.data();
const eventColor = profileData?.eventColor || colorMap.pink;
return {
...event,
start: event.allDay
? new Date(new Date(event.startDate.seconds * 1000).setHours(0, 0, 0, 0))
: new Date(event.startDate.seconds * 1000),
end: event.allDay
? new Date(new Date(event.endDate.seconds * 1000).setHours(0, 0, 0, 0))
: new Date(event.endDate.seconds * 1000),
hideHours: event.allDay,
eventColor,
notes: event.notes,
};
})
const snapshots = await Promise.all(
constraints.map(query => query.get())
);
const uniqueEvents = new Map();
const processedHashes = new Set();
const creatorIds = new Set();
console.log(`Events processing completed, returning ${processedEvents.length} events`);
return processedEvents;
snapshots.forEach(snapshot => {
snapshot.docs.forEach(doc => {
const event = doc.data();
const hash = createEventHash(event);
if (!processedHashes.has(hash)) {
processedHashes.add(hash);
creatorIds.add(event.creatorId);
uniqueEvents.set(doc.id, event);
}
});
});
const creatorIdsArray = Array.from(creatorIds);
const creatorProfiles = new Map();
for (let i = 0; i < creatorIdsArray.length; i += 10) {
const chunk = creatorIdsArray.slice(i, i + 10);
const profilesSnapshot = await db
.collection("Profiles")
.where(firestore.FieldPath.documentId(), "in", chunk)
.get();
profilesSnapshot.docs.forEach(doc => {
creatorProfiles.set(doc.id, doc.data()?.eventColor || colorMap.pink);
});
}
return Array.from(uniqueEvents.entries()).map(([id, event]) => {
const startSeconds = event.startDate.seconds;
const endSeconds = event.endDate.seconds;
return {
...event,
id,
start: event.allDay
? new Date(new Date(startSeconds * 1000).setHours(0, 0, 0, 0))
: new Date(startSeconds * 1000),
end: event.allDay
? new Date(new Date(endSeconds * 1000).setHours(0, 0, 0, 0))
: new Date(endSeconds * 1000),
hideHours: event.allDay,
eventColor: creatorProfiles.get(event.creatorId) || colorMap.pink,
notes: event.notes
};
});
},
staleTime: 5 * 60 * 1000,
cacheTime: 30 * 60 * 1000,
keepPreviousData: true,
onError: (error) => {
console.error('Error fetching events:', error);
}
gcTime: Infinity,
placeholderData: (previousData) => previousData,
});
};
};

View File

@ -1,4 +1,4 @@
import {useQuery} from "react-query";
import {useQuery} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import {useAuthContext} from "@/contexts/AuthContext";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";

View File

@ -1,5 +1,5 @@
import { useAuthContext } from "@/contexts/AuthContext";
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import { IFeedback } from "@/contexts/FeedbackContext";

View File

@ -1,4 +1,4 @@
import {useQuery} from "react-query";
import {useQuery} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import {useAuthContext} from "@/contexts/AuthContext";

View File

@ -1,4 +1,4 @@
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
export const useGetHouseholdName = (familyId: string) => {

View File

@ -1,5 +1,5 @@
import { useAuthContext } from "@/contexts/AuthContext";
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import { IBrainDump } from "@/contexts/DumpContext";

View File

@ -1,4 +1,4 @@
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import { useAuthContext } from "@/contexts/AuthContext";

View File

@ -1,4 +1,4 @@
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import firestore, {or, query, where} from "@react-native-firebase/firestore";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { IToDo } from "@/hooks/firebase/types/todoData";

View File

@ -1,4 +1,4 @@
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
import firestore from "@react-native-firebase/firestore";

View File

@ -1,4 +1,4 @@
import {useMutation} from "react-query";
import {useMutation} from "@tanstack/react-query";
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
import auth from "@react-native-firebase/auth";
import {useAuthContext} from "@/contexts/AuthContext";

View File

@ -1,4 +1,4 @@
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import functions from '@react-native-firebase/functions';
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { HttpsCallableResult } from "@firebase/functions";

View File

@ -1,4 +1,4 @@
import {useMutation} from "react-query";
import {useMutation} from "@tanstack/react-query";
import auth from "@react-native-firebase/auth";
export const useResetPassword = () => {

View File

@ -1,5 +1,5 @@
import {useAuthContext} from "@/contexts/AuthContext";
import {useMutation} from "react-query";
import {useMutation} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import {FirebaseAuthTypes} from "@react-native-firebase/auth";

View File

@ -1,4 +1,4 @@
import { useMutation } from "react-query";
import { useMutation } from "@tanstack/react-query";
import auth from "@react-native-firebase/auth";
export const useSignIn = () => {

View File

@ -1,4 +1,4 @@
import {useMutation} from "react-query";
import {useMutation} from "@tanstack/react-query";
import auth from "@react-native-firebase/auth";
import {useRouter} from "expo-router";

View File

@ -1,4 +1,4 @@
import {useMutation} from "react-query";
import {useMutation} from "@tanstack/react-query";
import auth from "@react-native-firebase/auth";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import {useSetUserData} from "./useSetUserData";

View File

@ -1,4 +1,4 @@
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import {EventData} from "@/hooks/firebase/types/eventData";

View File

@ -1,5 +1,5 @@
import { useAuthContext } from "@/contexts/AuthContext";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import { IFeedback } from "@/contexts/FeedbackContext";

View File

@ -1,4 +1,4 @@
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import {IGrocery} from "@/hooks/firebase/types/groceryData";

View File

@ -1,5 +1,5 @@
import firestore from "@react-native-firebase/firestore";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
export const useUpdateHouseholdName = () => {
const queryClient = useQueryClient();

View File

@ -1,5 +1,5 @@
import { useAuthContext } from "@/contexts/AuthContext";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import { IBrainDump } from "@/contexts/DumpContext";

View File

@ -1,4 +1,4 @@
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import firestore from "@react-native-firebase/firestore";

View File

@ -1,4 +1,4 @@
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import firestore from "@react-native-firebase/firestore";
import {IToDo, REPEAT_TYPE} from "@/hooks/firebase/types/todoData";
import {addDays, addMonths, addWeeks, addYears, compareAsc, format, subDays} from "date-fns";

View File

@ -1,6 +1,6 @@
import firestore from "@react-native-firebase/firestore";
import {FirebaseAuthTypes} from "@react-native-firebase/auth";
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {useAuthContext} from "@/contexts/AuthContext";
import {UserProfile} from "@/hooks/firebase/types/profileTypes";

View File

@ -9,7 +9,7 @@ import * as Google from "expo-auth-session/providers/google";
import * as AuthSession from "expo-auth-session";
import * as AppleAuthentication from "expo-apple-authentication";
import * as Notifications from 'expo-notifications';
import {useQueryClient} from "react-query";
import {useQueryClient} from "@tanstack/react-query";
import {AppleAccount, GoogleAccount, MicrosoftAccount} from "@/hooks/firebase/types/profileTypes";
const googleConfig = {

View File

@ -1,4 +1,4 @@
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {useAuthContext} from "@/contexts/AuthContext";
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
import {fetchiPhoneCalendarEvents} from "@/calendar-integration/apple-calendar-utils";
@ -23,7 +23,6 @@ export const useFetchAndSaveAppleEvents = () => {
timeMax
);
console.log(response);
const items = response ?? [];
await createEventsFromProvider(items);
} catch (error) {

View File

@ -1,4 +1,4 @@
import {useMutation, useQueryClient} from "react-query";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {useAuthContext} from "@/contexts/AuthContext";
import functions from "@react-native-firebase/functions";

View File

@ -1,4 +1,4 @@
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuthContext } from "@/contexts/AuthContext";
import { useSetUserData } from "@/hooks/firebase/useSetUserData";
import functions from '@react-native-firebase/functions';

View File

@ -333,6 +333,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
@ -375,6 +376,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",

View File

@ -47,7 +47,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>74</string>
<string>100</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>

View File

@ -29,6 +29,8 @@
</constraints>
<color key="backgroundColor" name="SplashScreenBackground"/>
<color key="backgroundColor" name="SplashScreenBackground"/>
<color key="backgroundColor" name="SplashScreenBackground"/>
<color key="backgroundColor" name="SplashScreenBackground"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -44,5 +46,11 @@
<namedColor name="SplashScreenBackground">
<color alpha="1.000" blue="1.00000000000000" green="1.00000000000000" red="1.00000000000000" customColorSpace="sRGB" colorSpace="custom"/>
</namedColor>
<namedColor name="SplashScreenBackground">
<color alpha="1.000" blue="1.00000000000000" green="1.00000000000000" red="1.00000000000000" customColorSpace="sRGB" colorSpace="custom"/>
</namedColor>
<namedColor name="SplashScreenBackground">
<color alpha="1.000" blue="1.00000000000000" green="1.00000000000000" red="1.00000000000000" customColorSpace="sRGB" colorSpace="custom"/>
</namedColor>
</resources>
</document>

View File

@ -1403,6 +1403,8 @@ PODS:
- ExpoModulesCore
- ExpoFont (13.0.1):
- ExpoModulesCore
- ExpoHaptics (14.0.0):
- ExpoModulesCore
- ExpoHead (4.0.11):
- ExpoModulesCore
- ExpoImagePicker (16.0.3):
@ -3263,6 +3265,8 @@ PODS:
- ReactNativeUiLib (4.3.2):
- React
- RecaptchaInterop (100.0.0)
- RNCAsyncStorage (2.1.0):
- React-Core
- RNDateTimePicker (8.2.0):
- React-Core
- RNFBApp (21.6.1):
@ -3454,6 +3458,7 @@ DEPENDENCIES:
- ExpoDevice (from `../node_modules/expo-device/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoFont (from `../node_modules/expo-font/ios`)
- ExpoHaptics (from `../node_modules/expo-haptics/ios`)
- ExpoHead (from `../node_modules/expo-router/ios`)
- ExpoImagePicker (from `../node_modules/expo-image-picker/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
@ -3536,6 +3541,7 @@ DEPENDENCIES:
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- ReactNativeKeyboardManager (from `../node_modules/react-native-keyboard-manager`)
- ReactNativeUiLib (from `../node_modules/react-native-ui-lib`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBAuth (from `../node_modules/@react-native-firebase/auth`)"
@ -3632,6 +3638,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-file-system/ios"
ExpoFont:
:path: "../node_modules/expo-font/ios"
ExpoHaptics:
:path: "../node_modules/expo-haptics/ios"
ExpoHead:
:path: "../node_modules/expo-router/ios"
ExpoImagePicker:
@ -3794,6 +3802,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-keyboard-manager"
ReactNativeUiLib:
:path: "../node_modules/react-native-ui-lib"
RNCAsyncStorage:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNDateTimePicker:
:path: "../node_modules/@react-native-community/datetimepicker"
RNFBApp:
@ -3851,6 +3861,7 @@ SPEC CHECKSUMS:
ExpoDevice: e24dd19a43065182eb246bc0e7f84186862f1e83
ExpoFileSystem: dc2679a2b5d4c465ca881129074da95faee943d5
ExpoFont: 7522d869d84ee2ee8093ee997fef5b86f85d856b
ExpoHaptics: e636188d1d5f7ccb79f3c1bfab47aaf5a1768c73
ExpoHead: c2695f9e8d685d6fa76e7a42006b0b3420cf2d50
ExpoImagePicker: 440a560c9bfc63edae854d08b9f12f75bd11c112
ExpoKeepAwake: 783e68647b969b210a786047c3daa7b753dcac1f
@ -3959,6 +3970,7 @@ SPEC CHECKSUMS:
ReactNativeKeyboardManager: 704d89bde3cb1e0f432bc273a44eec96eab9d90f
ReactNativeUiLib: acf5e70a64140d6f38a969978f090c16862c2d3d
RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21
RNCAsyncStorage: cc6479c4acd84cc7004946946c8afe30b018184d
RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14
RNFBApp: a3c9c3b23aa2d77001ce62b5a997325927593c12
RNFBAuth: 15a007eecd6ba7b9e52b45e2b41cdf6fe9f13968

View File

@ -35,6 +35,8 @@
"@expo-google-fonts/plus-jakarta-sans": "^0.2.3",
"@expo-google-fonts/poppins": "^0.2.3",
"@expo/vector-icons": "^14.0.2",
"@howljs/calendar-kit": "^2.2.1",
"@react-native-async-storage/async-storage": "^2.1.0",
"@react-native-community/blur": "^4.4.0",
"@react-native-community/datetimepicker": "8.2.0",
"@react-native-firebase/app": "^21.6.1",
@ -47,6 +49,9 @@
"@react-native/assets-registry": "^0.76.3",
"@react-navigation/drawer": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@tanstack/query-async-storage-persister": "^5.62.7",
"@tanstack/react-query": "^5.62.7",
"@tanstack/react-query-persist-client": "^5.62.7",
"date-fns": "^3.6.0",
"debounce": "^2.1.1",
"expo": "^52.0.17",
@ -61,6 +66,7 @@
"expo-dev-client": "~5.0.5",
"expo-device": "~7.0.1",
"expo-font": "~13.0.1",
"expo-haptics": "~14.0.0",
"expo-image-picker": "~16.0.3",
"expo-linking": "~7.0.3",
"expo-localization": "~16.0.0",
@ -98,7 +104,6 @@
"react-native-toast-message": "^2.2.1",
"react-native-ui-lib": "^7.27.0",
"react-native-web": "~0.19.10",
"react-query": "^3.39.3",
"timezonecomplete": "^5.13.1",
"tzdata": "^1.0.42"
},
@ -110,6 +115,7 @@
"@types/react-native-onboarding-swiper": "^1.1.9",
"@types/react-test-renderer": "^18.0.7",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-transform-remove-console": "^6.9.4",
"jest": "^29.2.1",
"jest-expo": "~52.0.2",
"postinstall-postinstall": "^2.1.0",

184
yarn.lock
View File

@ -845,7 +845,7 @@
pirates "^4.0.6"
source-map-support "^0.5.16"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.8", "@babel/runtime@^7.25.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.0", "@babel/runtime@^7.21.0", "@babel/runtime@^7.25.0", "@babel/runtime@^7.8.4":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
@ -2133,6 +2133,17 @@
dependencies:
"@hapi/hoek" "^9.0.0"
"@howljs/calendar-kit@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@howljs/calendar-kit/-/calendar-kit-2.2.1.tgz#125a3984e04a8b2f4f889c2be859fc88f779fbc5"
integrity sha512-3ZgPS46/14pfZ/gBbMsdrSpCy/jTfVpgeHcvOHW7itflUnFBOSCRhGc4cWRd27vD9gIAu63PdWngQ34p/5EwFw==
dependencies:
lodash.debounce "^4.0.8"
lodash.isequal "^4.5.0"
lodash.merge "^4.6.2"
luxon "^3.4.4"
rrule "^2.8.1"
"@ide/backoff@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@ide/backoff/-/backoff-1.0.0.tgz#466842c25bd4a4833e0642fab41ccff064010176"
@ -2541,6 +2552,13 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@react-native-async-storage/async-storage@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-2.1.0.tgz#84ca82af320c16d3d8e617508ea523fe786b6781"
integrity sha512-eAGQGPTAuFNEoIQSB5j2Jh1zm5NPyBRTfjRMfCN0W1OakC5WIB5vsDyIQhUweKN9XOE2/V07lqTMGsL0dGXNkA==
dependencies:
merge-options "^3.0.4"
"@react-native-community/blur@^4.4.0":
version "4.4.1"
resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.4.1.tgz#72cbc0be5a84022c33091683ec6888925ebcca6e"
@ -3354,6 +3372,39 @@
dependencies:
"@sinonjs/commons" "^3.0.0"
"@tanstack/query-async-storage-persister@^5.62.7":
version "5.62.7"
resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.62.7.tgz#9d076294d8c231c61dccf333151443893246dd93"
integrity sha512-8qSJ1oTnGhCikPADWd35xrswyYtmkqYnakWgqeXjxL+F+qPGgsfexNUlBu9TNqo9eAP/1ia4Lt5Ks2fTsMzBgg==
dependencies:
"@tanstack/query-persist-client-core" "5.62.7"
"@tanstack/query-core@5.62.7":
version "5.62.7"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.7.tgz#c7f6d0131c08cd2f60e73ec6e7b70e2e9e335def"
integrity sha512-fgpfmwatsrUal6V+8EC2cxZIQVl9xvL7qYa03gsdsCy985UTUlS4N+/3hCzwR0PclYDqisca2AqR1BVgJGpUDA==
"@tanstack/query-persist-client-core@5.62.7":
version "5.62.7"
resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.62.7.tgz#3c03c46fe15db1098cb07b7c9867071c3d336c84"
integrity sha512-9HcaD9rEp2nGWnrw2osK5UCSKJbJKEdn+MEhVVfnUPSFN7MZFpFZxpRCHJi3fRpWOYsVeH9EFODX+aoJaniJMA==
dependencies:
"@tanstack/query-core" "5.62.7"
"@tanstack/react-query-persist-client@^5.62.7":
version "5.62.7"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.62.7.tgz#74f66da2c0c077df17709644cbaa517ffe402540"
integrity sha512-RmEJ3YvsK7lv1Of3CCXBgHDtoZVwHMtTKCTegZz+xijVJsgJaNNfel4YTpbQ0ydnWT2IcohdqnHUtBE6p1KCIA==
dependencies:
"@tanstack/query-persist-client-core" "5.62.7"
"@tanstack/react-query@^5.62.7":
version "5.62.7"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.7.tgz#8f253439a38ad6ce820bc6d42d89ca2556574d1a"
integrity sha512-+xCtP4UAFDTlRTYyEjLx0sRtWyr5GIk7TZjZwBu4YaNahi3Rt2oMyRqfpfVrtwsqY2sayP4iXVCwmC+ZqqFmuw==
dependencies:
"@tanstack/query-core" "5.62.7"
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@ -4170,6 +4221,11 @@ babel-plugin-transform-inline-environment-variables@^0.0.2:
resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-environment-variables/-/babel-plugin-transform-inline-environment-variables-0.0.2.tgz#1372475c6c87f3c4ce6046587730ee041bc784b9"
integrity sha512-8gobU7uuTIjz62aXTEZOH5yhuIPojNVAgLK0xnepdGS19aqOEphy7FVWBsojPa14yrQGM/w63uDox4thYcHCnA==
babel-plugin-transform-remove-console@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz#b980360c067384e24b357a588d807d3c83527780"
integrity sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg==
babel-preset-current-node-syntax@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30"
@ -4252,7 +4308,7 @@ better-opn@~3.0.2:
dependencies:
open "^8.0.4"
big-integer@1.6.x, big-integer@^1.6.16:
big-integer@1.6.x:
version "1.6.52"
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85"
integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==
@ -4359,20 +4415,6 @@ braces@^3.0.3:
dependencies:
fill-range "^7.1.1"
broadcast-channel@^3.4.1:
version "3.7.0"
resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937"
integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==
dependencies:
"@babel/runtime" "^7.7.2"
detect-node "^2.1.0"
js-sha3 "0.8.0"
microseconds "0.2.0"
nano-time "1.0.0"
oblivious-set "1.0.0"
rimraf "3.0.2"
unload "2.2.0"
browserslist@^4.24.0, browserslist@^4.24.2:
version "4.24.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
@ -5222,11 +5264,6 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
detect-node@^2.0.4, detect-node@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
diff-sequences@^29.6.3:
version "29.6.3"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
@ -5787,6 +5824,11 @@ expo-font@~13.0.1:
dependencies:
fontfaceobserver "^2.1.0"
expo-haptics@~14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-14.0.0.tgz#b3ccea2ed5c7f4c2505e2e8cbfa799091b185303"
integrity sha512-5tYJN+2axYF22BtG1elBQAV1aZPUOCtr9sItClfm4jDoekGiPCxZG/nylcA3DVh2bUHMSll4Y98qjFFFhwZ1Cw==
expo-image-loader@~5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-5.0.0.tgz#4f58a21ab26e40d6fccc211b664fd9fe21a5dcb8"
@ -7197,6 +7239,11 @@ is-path-inside@^3.0.2:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@ -7816,11 +7863,6 @@ jotai@^2.9.1:
resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.10.1.tgz#8d5598d06fa295110de0914f10bd1d10ea229723"
integrity sha512-4FycO+BOTl2auLyF2Chvi6KTDqdsdDDtpaL/WHQMs8f3KS1E3loiUShQzAzFA/sMU5cJ0hz/RT1xum9YbG/zaA==
js-sha3@0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -8270,6 +8312,11 @@ lodash.isboolean@^3.0.3:
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
@ -8290,6 +8337,11 @@ lodash.isstring@^4.0.1:
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
@ -8368,6 +8420,11 @@ lru-memoizer@^2.2.0:
lodash.clonedeep "^4.5.0"
lru-cache "6.0.0"
luxon@^3.4.4:
version "3.5.0"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20"
integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==
make-dir@^2.0.0, make-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@ -8395,14 +8452,6 @@ marky@^1.2.2:
resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0"
integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==
match-sorter@^6.0.2:
version "6.4.0"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.4.0.tgz#ae9c166cb3c9efd337690b3160c0e28cb8377c13"
integrity sha512-d4664ahzdL1QTTvmK1iI0JsrxWeJ6gn33qkYtnPg3mcn+naBLtXSgSPOe+X2vUgtgGwaAk3eiaj7gwKjjMAq+Q==
dependencies:
"@babel/runtime" "^7.23.8"
remove-accents "0.5.0"
md5-file@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-3.2.3.tgz#f9bceb941eca2214a4c0727f5e700314e770f06f"
@ -8463,6 +8512,13 @@ merge-descriptors@1.0.3:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
merge-options@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7"
integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==
dependencies:
is-plain-obj "^2.1.0"
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -8685,11 +8741,6 @@ micromatch@^4.0.2, micromatch@^4.0.4:
braces "^3.0.3"
picomatch "^2.3.1"
microseconds@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
@ -8850,13 +8901,6 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nano-time@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==
dependencies:
big-integer "^1.6.16"
nanoid@3.3.7, nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
@ -9051,11 +9095,6 @@ object.assign@^4.1.4, object.assign@^4.1.5:
has-symbols "^1.0.3"
object-keys "^1.1.1"
oblivious-set@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566"
integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==
on-finished@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
@ -10002,15 +10041,6 @@ react-native@0.76.3:
ws "^6.2.3"
yargs "^17.6.2"
react-query@^3.39.3:
version "3.39.3"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35"
integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==
dependencies:
"@babel/runtime" "^7.5.5"
broadcast-channel "^3.4.1"
match-sorter "^6.0.2"
react-refresh@^0.14.0, react-refresh@^0.14.2:
version "0.14.2"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
@ -10166,11 +10196,6 @@ regjsparser@^0.11.0:
dependencies:
jsesc "~3.0.2"
remove-accents@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687"
integrity sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==
remove-trailing-slash@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d"
@ -10293,13 +10318,6 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rimraf@3.0.2, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@ -10307,6 +10325,13 @@ rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
rimraf@~2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
@ -10314,6 +10339,13 @@ rimraf@~2.6.2:
dependencies:
glob "^7.1.3"
rrule@^2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.8.1.tgz#e8341a9ce3e68ce5b8da4d502e893cd9f286805e"
integrity sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==
dependencies:
tslib "^2.4.0"
rtl-detect@^1.0.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.1.2.tgz#ca7f0330af5c6bb626c15675c642ba85ad6273c6"
@ -11501,14 +11533,6 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unload@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7"
integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==
dependencies:
"@babel/runtime" "^7.6.2"
detect-node "^2.0.4"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"