Files
cally/components/pages/calendar/useFormattedEvents.ts
2024-12-15 18:45:04 +01:00

154 lines
4.7 KiB
TypeScript

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
});
};