Files
cally/components/pages/calendar/useFormattedEvents.ts
2025-02-02 22:28:40 +01:00

164 lines
4.9 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;
}
const createEventHash = (event: FormattedEvent): string => {
const startTime = 'date' in event.start ? event.start.date : event.start.dateTime;
const endTime = 'date' in event.end ? event.end.date : event.end.dateTime;
const str = `${startTime}-${endTime}-${event.title}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash.toString(36);
};
// Precompute time constants
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const PERIOD_IN_MS = 180 * 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[]> => {
if (!events.length) return [];
const currentRangeKey = getDateRangeKey(selectedDate.getTime());
const isTablet = Device.deviceType === DeviceType.TABLET;
const userId = selectedUser?.uid;
const uniqueEvents = new Map<string, FormattedEvent>();
const processedHashes = new Set<string>();
const CHUNK_SIZE = 100;
for (let i = 0; i < events.length; i += CHUNK_SIZE) {
const chunk = events.slice(i, i + CHUNK_SIZE);
await new Promise(resolve => setTimeout(resolve, 0));
for (const event of chunk) {
try {
if (isTablet && userId &&
!event.attendees?.includes(userId) &&
event.creatorId !== userId) {
continue;
}
const startDate = getValidDate(event.start, event.startDate);
if (!startDate) continue;
const rangeKey = getDateRangeKey(startDate.getTime());
if (Math.abs(rangeKey - currentRangeKey) > 1) continue;
const endDate = getValidDate(event.end, event.endDate);
if (!endDate) continue;
const formattedEvent = event.allDay ? {
id: event.id,
title: event.title,
start: { date: format(startDate, 'yyyy-MM-dd') },
end: { date: format(endDate, 'yyyy-MM-dd') },
color: event.eventColor
} : {
id: event.id,
title: event.title,
start: {
dateTime: startDate.toISOString(),
timeZone: TIME_ZONE
},
end: {
dateTime: endDate.toISOString(),
timeZone: TIME_ZONE
},
color: event.eventColor
};
const hash = createEventHash(formattedEvent);
if (!processedHashes.has(hash)) {
processedHashes.add(hash);
uniqueEvents.set(event.id, formattedEvent);
}
} catch (error) {
console.error('Error processing event:', event.id, error);
continue;
}
}
}
return Array.from(uniqueEvents.values());
};
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
});
};