Skip to content

Commit 149fa28

Browse files
committed
apple calender enablement wip
1 parent 5d1ab5f commit 149fa28

18 files changed

Lines changed: 229 additions & 149 deletions

File tree

apps/desktop/src/components/settings/calendar/configure/apple.tsx

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMutation, useQuery } from "@tanstack/react-query";
22
import { openUrl } from "@tauri-apps/plugin-opener";
33
import { AlertCircleIcon, ArrowRightIcon, CheckIcon } from "lucide-react";
4-
import { useMemo } from "react";
4+
import { useEffect, useMemo } from "react";
55

66
import {
77
commands as appleCalendarCommands,
@@ -188,43 +188,49 @@ function useAppleCalendarSelection() {
188188
const calendars = main.UI.useTable("calendars", main.STORE_ID);
189189
const { user_id } = main.UI.useValues(main.STORE_ID);
190190

191-
const { mutate: syncCalendars, isPending } = useMutation({
192-
mutationKey: ["appleCalendars", "sync"],
193-
mutationFn: async () => {
191+
const {
192+
data: incomingCalendars,
193+
refetch,
194+
isFetching,
195+
} = useQuery({
196+
queryKey: ["appleCalendars"],
197+
queryFn: async () => {
194198
const [result] = await Promise.all([
195199
appleCalendarCommands.listCalendars(),
196-
new Promise((resolve) => setTimeout(resolve, 250)),
200+
new Promise((resolve) => setTimeout(resolve, 150)),
197201
]);
202+
198203
if (result.status === "error") {
199204
throw new Error(result.error);
200205
}
201206
return result.data;
202207
},
203-
onSuccess: (incomingCalendars) => {
204-
if (!store || !user_id) return;
208+
});
205209

206-
store.transaction(() => {
207-
for (const cal of incomingCalendars) {
208-
const existingRowId = findCalendarByTrackingId(store, cal.id);
209-
const rowId = existingRowId ?? crypto.randomUUID();
210-
const existing = existingRowId
211-
? store.getRow("calendars", existingRowId)
212-
: null;
210+
useEffect(() => {
211+
if (!incomingCalendars || !store || !user_id) return;
213212

214-
store.setRow("calendars", rowId, {
215-
user_id,
216-
created_at: existing?.created_at || new Date().toISOString(),
217-
tracking_id_calendar: cal.id,
218-
name: cal.title,
219-
enabled: existing?.enabled ?? false,
220-
provider: "apple",
221-
source: cal.source.title,
222-
color: colorToCSS(cal.color),
223-
});
224-
}
225-
});
226-
},
227-
});
213+
store.transaction(() => {
214+
for (const cal of incomingCalendars) {
215+
const existingRowId = findCalendarByTrackingId(store, cal.id);
216+
const rowId = existingRowId ?? crypto.randomUUID();
217+
const existing = existingRowId
218+
? store.getRow("calendars", existingRowId)
219+
: null;
220+
221+
store.setRow("calendars", rowId, {
222+
user_id,
223+
created_at: existing?.created_at || new Date().toISOString(),
224+
tracking_id_calendar: cal.id,
225+
name: cal.title,
226+
enabled: existing?.enabled ?? false,
227+
provider: "apple",
228+
source: cal.source.title,
229+
color: colorToCSS(cal.color),
230+
});
231+
}
232+
});
233+
}, [incomingCalendars, store, user_id]);
228234

229235
const groups = useMemo((): CalendarGroup[] => {
230236
const appleCalendars = Object.entries(calendars).filter(
@@ -256,8 +262,8 @@ function useAppleCalendarSelection() {
256262
return {
257263
groups,
258264
handleToggle,
259-
handleRefresh: syncCalendars,
260-
isLoading: isPending,
265+
handleRefresh: refetch,
266+
isLoading: isFetching,
261267
};
262268
}
263269

@@ -281,7 +287,7 @@ function DocumentationLink({ href }: { href: string }) {
281287
onClick={() => openUrl(href)}
282288
className="mb-3 text-xs text-neutral-500 hover:text-neutral-700 hover:underline"
283289
>
284-
Read the docs
290+
Read the docs
285291
</button>
286292
);
287293
}

apps/desktop/src/components/settings/calendar/configure/shared.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RefreshCwIcon } from "lucide-react";
1+
import { CalendarOffIcon, RefreshCwIcon } from "lucide-react";
22

33
import { Button } from "@hypr/ui/components/ui/button";
44
import { Switch } from "@hypr/ui/components/ui/switch";
@@ -31,8 +31,9 @@ export function CalendarSelection({
3131
}: CalendarSelectionProps) {
3232
if (groups.length === 0) {
3333
return (
34-
<div className="py-4 text-center text-sm text-neutral-500">
35-
No calendars found
34+
<div className="flex flex-col items-center justify-center py-8 px-4 border border-dashed border-neutral-200 rounded-lg bg-neutral-50/50">
35+
<CalendarOffIcon className="size-8 text-neutral-300 mb-3" />
36+
<p className="text-sm text-neutral-500">No calendars found</p>
3637
</div>
3738
);
3839
}
@@ -54,13 +55,6 @@ export function CalendarSelection({
5455
</Button>
5556
</div>
5657
<div className="space-y-4">
57-
<div className="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
58-
<p className="text-xs text-amber-700">
59-
Event fetching is not implemented yet. Calendar selection will be
60-
used once event syncing is available.
61-
</p>
62-
</div>
63-
6458
{groups.map((group) => (
6559
<div key={group.sourceName}>
6660
<h5 className="text-xs font-medium text-neutral-500 mb-2">

apps/desktop/src/components/settings/calendar/shared.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type CalendarProviderId = (typeof _PROVIDERS)[number]["id"];
1717

1818
const _PROVIDERS = [
1919
{
20-
disabled: true,
20+
disabled: false,
2121
id: "apple",
2222
displayName: "Apple",
2323
badge: "Almost Done, really",

apps/desktop/src/components/settings/general/notification.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,17 +236,14 @@ export function NotificationSettingsView() {
236236
{(field) => (
237237
<div className="flex items-start justify-between gap-4">
238238
<div className="flex-1">
239-
<h3 className="mb-1 text-sm font-medium">
240-
Event notifications (Not-available)
241-
</h3>
239+
<h3 className="mb-1 text-sm font-medium">Event notifications</h3>
242240
<p className="text-xs text-neutral-600">
243-
Get notified about upcoming calendar events
241+
Get notified 5 minutes before calendar events start
244242
</p>
245243
</div>
246244
<Switch
247-
checked={false}
245+
checked={field.state.value}
248246
onCheckedChange={field.handleChange}
249-
disabled
250247
/>
251248
</div>
252249
)}
Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,51 @@
1+
import { useRef } from "react";
12
import type { Queries } from "tinybase/with-schemas";
23
import { useScheduleTaskRun, useSetTask } from "tinytick/ui-react";
34

45
import {
56
CALENDAR_SYNC_TASK_ID,
67
syncCalendarEvents,
78
} from "../services/apple-calendar";
9+
import {
10+
checkEventNotifications,
11+
EVENT_NOTIFICATION_INTERVAL,
12+
EVENT_NOTIFICATION_TASK_ID,
13+
} from "../services/event-notification";
814
import * as main from "../store/tinybase/main";
15+
import * as settings from "../store/tinybase/settings";
916

1017
const CALENDAR_SYNC_INTERVAL = 60 * 1000; // 60 sec
1118

1219
export function TaskManager() {
1320
const store = main.UI.useStore(main.STORE_ID);
1421
const queries = main.UI.useQueries(main.STORE_ID);
1522

23+
const settingsStore = settings.UI.useStore(settings.STORE_ID);
24+
const notifiedEventsRef = useRef<Set<string>>(new Set());
25+
1626
useSetTask(CALENDAR_SYNC_TASK_ID, async () => {
17-
// TODO: Not ready yet
18-
if (false) {
19-
await syncCalendarEvents(
20-
store as main.Store,
21-
queries as Queries<main.Schemas>,
22-
);
23-
}
27+
await syncCalendarEvents(
28+
store as main.Store,
29+
queries as Queries<main.Schemas>,
30+
);
2431
});
2532

2633
useScheduleTaskRun(CALENDAR_SYNC_TASK_ID, undefined, 0, {
2734
repeatDelay: CALENDAR_SYNC_INTERVAL,
2835
});
2936

37+
useSetTask(EVENT_NOTIFICATION_TASK_ID, async () => {
38+
if (!store || !settingsStore) return;
39+
checkEventNotifications(
40+
store as main.Store,
41+
settingsStore as settings.Store,
42+
notifiedEventsRef.current,
43+
);
44+
});
45+
46+
useScheduleTaskRun(EVENT_NOTIFICATION_TASK_ID, undefined, 0, {
47+
repeatDelay: EVENT_NOTIFICATION_INTERVAL,
48+
});
49+
3050
return null;
3151
}

apps/desktop/src/services/apple-calendar/fetch/existing.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export function fetchExistingEvents(ctx: Ctx): ExistingEvent[] {
3131
meeting_link: event.meeting_link as string | undefined,
3232
description: event.description as string | undefined,
3333
note: event.note as string | undefined,
34-
participants: event.participants as string | undefined,
3534
});
3635
}
3736
});

apps/desktop/src/services/apple-calendar/fetch/incoming.ts

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import type { AppleEvent, Participant } from "@hypr/plugin-apple-calendar";
22
import { commands as appleCalendarCommands } from "@hypr/plugin-apple-calendar";
33
import { commands as miscCommands } from "@hypr/plugin-misc";
4-
import type { EventParticipant } from "@hypr/store";
54

65
import type { Ctx } from "../ctx";
7-
import type { IncomingEvent } from "./types";
6+
import type {
7+
EventParticipant,
8+
IncomingEvent,
9+
IncomingParticipants,
10+
} from "./types";
811

9-
export async function fetchIncomingEvents(ctx: Ctx): Promise<IncomingEvent[]> {
12+
export async function fetchIncomingEvents(ctx: Ctx): Promise<{
13+
events: IncomingEvent[];
14+
participants: IncomingParticipants;
15+
}> {
1016
const trackingIds = Array.from(ctx.calendarTrackingIdToId.keys());
1117

1218
const results = await Promise.all(
@@ -25,36 +31,51 @@ export async function fetchIncomingEvents(ctx: Ctx): Promise<IncomingEvent[]> {
2531
}),
2632
);
2733

28-
const events = results.flat();
34+
const appleEvents = results.flat();
35+
const events: IncomingEvent[] = [];
36+
const participants: IncomingParticipants = new Map();
2937

30-
return Promise.all(events.map(normalizeAppleEvent));
38+
for (const appleEvent of appleEvents) {
39+
const { event, eventParticipants } = await normalizeAppleEvent(appleEvent);
40+
events.push(event);
41+
if (eventParticipants.length > 0) {
42+
participants.set(event.tracking_id_event, eventParticipants);
43+
}
44+
}
45+
46+
return { events, participants };
3147
}
3248

33-
async function normalizeAppleEvent(event: AppleEvent): Promise<IncomingEvent> {
49+
async function normalizeAppleEvent(appleEvent: AppleEvent): Promise<{
50+
event: IncomingEvent;
51+
eventParticipants: EventParticipant[];
52+
}> {
3453
const meetingLink =
35-
event.url ?? (await extractMeetingLink(event.notes, event.location));
54+
appleEvent.url ??
55+
(await extractMeetingLink(appleEvent.notes, appleEvent.location));
3656

37-
const participants: EventParticipant[] = [];
57+
const eventParticipants: EventParticipant[] = [];
3858

39-
if (event.organizer) {
40-
participants.push(normalizeParticipant(event.organizer, true));
59+
if (appleEvent.organizer) {
60+
eventParticipants.push(normalizeParticipant(appleEvent.organizer, true));
4161
}
4262

43-
for (const attendee of event.attendees) {
44-
participants.push(normalizeParticipant(attendee, false));
63+
for (const attendee of appleEvent.attendees) {
64+
eventParticipants.push(normalizeParticipant(attendee, false));
4565
}
4666

4767
return {
48-
tracking_id_event: event.event_identifier,
49-
tracking_id_calendar: event.calendar.id,
50-
title: event.title,
51-
started_at: event.start_date,
52-
ended_at: event.end_date,
53-
location: event.location ?? undefined,
54-
meeting_link: meetingLink ?? undefined,
55-
description: event.notes ?? undefined,
56-
participants:
57-
participants.length > 0 ? JSON.stringify(participants) : undefined,
68+
event: {
69+
tracking_id_event: appleEvent.event_identifier,
70+
tracking_id_calendar: appleEvent.calendar.id,
71+
title: appleEvent.title,
72+
started_at: appleEvent.start_date,
73+
ended_at: appleEvent.end_date,
74+
location: appleEvent.location ?? undefined,
75+
meeting_link: meetingLink ?? undefined,
76+
description: appleEvent.notes ?? undefined,
77+
},
78+
eventParticipants,
5879
};
5980
}
6081

apps/desktop/src/services/apple-calendar/fetch/types.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { EventStorage } from "@hypr/store";
22

3+
export type EventParticipant = {
4+
name?: string;
5+
email?: string;
6+
is_organizer?: boolean;
7+
is_current_user?: boolean;
8+
};
9+
310
export type IncomingEvent = {
411
tracking_id_event: string;
512
tracking_id_calendar: string;
@@ -9,9 +16,10 @@ export type IncomingEvent = {
916
location?: string;
1017
meeting_link?: string;
1118
description?: string;
12-
participants?: string;
1319
};
1420

21+
export type IncomingParticipants = Map<string, EventParticipant[]>;
22+
1523
export type ExistingEvent = {
1624
id: string;
1725
tracking_id_event?: string;

apps/desktop/src/services/apple-calendar/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ async function run(store: Store, queries: Queries<Schemas>) {
2828
return null;
2929
}
3030

31-
const incoming = await fetchIncomingEvents(ctx);
31+
const { events: incoming, participants: incomingParticipants } =
32+
await fetchIncomingEvents(ctx);
3233
const existing = fetchExistingEvents(ctx);
3334

34-
const out = syncEvents(ctx, { incoming, existing });
35-
const { addedEventIds } = executeForEventsSync(ctx, out);
35+
const eventsOut = syncEvents(ctx, { incoming, existing });
36+
const { trackingIdToEventId } = executeForEventsSync(ctx, eventsOut);
3637

3738
const participantsOut = syncParticipants(ctx, {
38-
eventIds: [...out.toUpdate.map((e) => e.id), ...addedEventIds],
39+
incomingParticipants,
40+
trackingIdToEventId,
3941
});
4042
executeForParticipantsSync(ctx, participantsOut);
4143
}

0 commit comments

Comments
 (0)