Skip to content

Commit 9ff033b

Browse files
committed
extract apple-calender sync logic under sevices (#2505)
1 parent 1917fe8 commit 9ff033b

File tree

15 files changed

+420
-152
lines changed

15 files changed

+420
-152
lines changed

apps/desktop/src/components/main/sidebar/timeline/task.ts

Lines changed: 0 additions & 147 deletions
This file was deleted.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
import { Button } from "@hypr/ui/components/ui/button";
99
import { Spinner } from "@hypr/ui/components/ui/spinner";
1010

11+
import { CALENDAR_SYNC_TASK_ID } from "../../../services/apple-calendar";
1112
import * as main from "../../../store/tinybase/main";
12-
import { CALENDAR_SYNC_TASK_ID } from "../../main/sidebar/timeline/task";
1313

1414
export function CalendarStatus() {
1515
const calendars = main.UI.useTable("calendars", main.STORE_ID);

apps/desktop/src/components/task-manager.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1+
import type { Queries } from "tinybase/with-schemas";
12
import { useScheduleTaskRun, useSetTask } from "tinytick/ui-react";
23

3-
import * as main from "../store/tinybase/main";
44
import {
55
CALENDAR_SYNC_TASK_ID,
66
syncCalendarEvents,
7-
} from "./main/sidebar/timeline/task";
7+
} from "../services/apple-calendar";
8+
import * as main from "../store/tinybase/main";
89

910
const CALENDAR_SYNC_INTERVAL = 60 * 1000; // 60 sec
1011

1112
export function TaskManager() {
1213
const store = main.UI.useStore(main.STORE_ID);
14+
const queries = main.UI.useQueries(main.STORE_ID);
1315

1416
useSetTask(CALENDAR_SYNC_TASK_ID, async () => {
15-
if (store) {
16-
await syncCalendarEvents(store as main.Store);
17+
if (store && queries) {
18+
await syncCalendarEvents(
19+
store as main.Store,
20+
queries as Queries<main.Schemas>,
21+
);
1722
}
1823
});
1924

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Queries } from "tinybase/with-schemas";
2+
3+
import { QUERIES, type Schemas, type Store } from "../../store/tinybase/main";
4+
5+
export interface Ctx {
6+
store: Store;
7+
userId: string;
8+
from: Date;
9+
to: Date;
10+
calendarIds: string[];
11+
}
12+
13+
export function createCtx(store: Store, queries: Queries<Schemas>): Ctx | null {
14+
const resultTable = queries.getResultTable(QUERIES.enabledAppleCalendars);
15+
16+
const calendarIds = Object.keys(resultTable);
17+
18+
if (calendarIds.length === 0) {
19+
return null;
20+
}
21+
22+
const userId = store.getValue("user_id");
23+
if (!userId) {
24+
return null;
25+
}
26+
27+
const { from, to } = getRange();
28+
29+
return {
30+
store,
31+
userId: String(userId),
32+
from,
33+
to,
34+
calendarIds,
35+
};
36+
}
37+
38+
const getRange = () => {
39+
const now = new Date();
40+
const from = new Date(now);
41+
from.setDate(from.getDate() - 7);
42+
const to = new Date(now);
43+
to.setDate(to.getDate() + 30);
44+
return { from, to };
45+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Ctx } from "../ctx";
2+
import type { ExistingEvent } from "./types";
3+
4+
export function fetchExistingEvents(ctx: Ctx): Array<ExistingEvent> {
5+
const enabledCalendarIds = new Set(ctx.calendarIds);
6+
const events: Array<ExistingEvent> = [];
7+
8+
ctx.store.forEachRow("events", (rowId, _forEachCell) => {
9+
const event = ctx.store.getRow("events", rowId);
10+
if (!event) return;
11+
12+
const calendarId = event.calendar_id as string | undefined;
13+
if (!calendarId || !enabledCalendarIds.has(calendarId)) {
14+
return;
15+
}
16+
17+
const startedAt = event.started_at as string | undefined;
18+
if (!startedAt) return;
19+
20+
const eventDate = new Date(startedAt);
21+
if (eventDate >= ctx.from && eventDate <= ctx.to) {
22+
events.push({
23+
id: rowId,
24+
user_id: event.user_id as string | undefined,
25+
created_at: event.created_at as string | undefined,
26+
calendar_id: calendarId,
27+
title: event.title as string | undefined,
28+
started_at: startedAt,
29+
ended_at: event.ended_at as string | undefined,
30+
location: event.location as string | undefined,
31+
meeting_link: event.meeting_link as string | undefined,
32+
description: event.description as string | undefined,
33+
note: event.note as string | undefined,
34+
participants: event.participants as string | undefined,
35+
});
36+
}
37+
});
38+
39+
return events;
40+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { AppleEvent, Participant } from "@hypr/plugin-apple-calendar";
2+
import { commands as appleCalendarCommands } from "@hypr/plugin-apple-calendar";
3+
import { commands as miscCommands } from "@hypr/plugin-misc";
4+
import type { EventParticipant } from "@hypr/store";
5+
6+
import type { Ctx } from "../ctx";
7+
import type { IncomingEvent } from "./types";
8+
9+
export async function fetchIncomingEvents(
10+
ctx: Ctx,
11+
): Promise<Array<IncomingEvent>> {
12+
const results = await Promise.all(
13+
ctx.calendarIds.map(async (calendarId) => {
14+
const result = await appleCalendarCommands.listEvents({
15+
calendar_tracking_id: calendarId,
16+
from: ctx.from.toISOString(),
17+
to: ctx.to.toISOString(),
18+
});
19+
20+
if (result.status === "error") {
21+
return [];
22+
}
23+
24+
return result.data;
25+
}),
26+
);
27+
28+
const events = results.flat();
29+
30+
return Promise.all(events.map(normalizeAppleEvent));
31+
}
32+
33+
async function normalizeAppleEvent(event: AppleEvent): Promise<IncomingEvent> {
34+
const meetingLink =
35+
event.url ?? (await extractMeetingLink(event.notes, event.location));
36+
37+
const participants: EventParticipant[] = [];
38+
39+
if (event.organizer) {
40+
participants.push(normalizeParticipant(event.organizer, true));
41+
}
42+
43+
for (const attendee of event.attendees) {
44+
participants.push(normalizeParticipant(attendee, false));
45+
}
46+
47+
return {
48+
id: event.event_identifier,
49+
calendar_id: 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,
58+
};
59+
}
60+
61+
async function extractMeetingLink(
62+
...texts: (string | undefined | null)[]
63+
): Promise<string | undefined> {
64+
for (const text of texts) {
65+
if (!text) continue;
66+
const result = await miscCommands.parseMeetingLink(text);
67+
if (result) return result;
68+
}
69+
return undefined;
70+
}
71+
72+
function normalizeParticipant(
73+
participant: Participant,
74+
isOrganizer: boolean,
75+
): EventParticipant {
76+
return {
77+
name: participant.name ?? undefined,
78+
email: participant.email ?? undefined,
79+
is_organizer: isOrganizer,
80+
is_current_user: participant.is_current_user,
81+
};
82+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { fetchExistingEvents } from "./existing";
2+
export { fetchIncomingEvents } from "./incoming";
3+
export type { ExistingEvent, IncomingEvent } from "./types";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { EventStorage } from "@hypr/store";
2+
3+
type EventBaseForSync = { id: string };
4+
5+
export type IncomingEvent = EventBaseForSync &
6+
Omit<EventStorage, "user_id" | "created_at">;
7+
8+
export type ExistingEvent = EventBaseForSync & EventStorage;

0 commit comments

Comments
 (0)