Skip to content

Commit afbda31

Browse files
feat: show next scheduled sync time in calendar status
- Track when calendar sync task completes and calculate next run time - Display countdown to next sync (e.g., 'Next sync in 45s') - Use TinyTick's useScheduledTaskRunIds and getTaskRunInfo to get scheduled timestamp - Export CALENDAR_SYNC_INTERVAL from apple-calendar service for reuse Co-Authored-By: yujonglee <[email protected]>
1 parent 1d16ec8 commit afbda31

File tree

3 files changed

+105
-15
lines changed

3 files changed

+105
-15
lines changed

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

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,132 @@
11
import { RefreshCwIcon } from "lucide-react";
2-
import { useCallback, useMemo, useState } from "react";
2+
import { useCallback, useEffect, useMemo, useState } from "react";
33
import {
4+
useManager,
5+
useRunningTaskRunIds,
6+
useScheduledTaskRunIds,
47
useScheduleTaskRunCallback,
5-
useTaskRunRunning,
68
} from "tinytick/ui-react";
79

810
import { Button } from "@hypr/ui/components/ui/button";
911
import { Spinner } from "@hypr/ui/components/ui/spinner";
1012

11-
import { CALENDAR_SYNC_TASK_ID } from "../../../services/apple-calendar";
13+
import {
14+
CALENDAR_SYNC_INTERVAL,
15+
CALENDAR_SYNC_TASK_ID,
16+
} from "../../../services/apple-calendar";
1217
import * as main from "../../../store/tinybase/main";
1318

1419
export function CalendarStatus() {
20+
const manager = useManager();
1521
const calendars = main.UI.useTable("calendars", main.STORE_ID);
1622

1723
const selectedCount = useMemo(() => {
1824
return Object.values(calendars).filter((cal) => cal.enabled).length;
1925
}, [calendars]);
2026

21-
const [currentTaskRunId, setCurrentTaskRunId] = useState<string | undefined>(
22-
undefined,
23-
);
24-
2527
const scheduleTaskRun = useScheduleTaskRunCallback(
2628
CALENDAR_SYNC_TASK_ID,
2729
undefined,
2830
0,
2931
);
3032

31-
const isRunning = useTaskRunRunning(currentTaskRunId ?? "");
33+
const runningTaskRunIds = useRunningTaskRunIds();
34+
const scheduledTaskRunIds = useScheduledTaskRunIds();
35+
36+
const [lastCompletedAt, setLastCompletedAt] = useState<number | null>(null);
37+
const [nextRunIn, setNextRunIn] = useState<number | null>(null);
38+
39+
const isRunning = useMemo(() => {
40+
if (!manager) return false;
41+
return runningTaskRunIds.some((id) => {
42+
const info = manager.getTaskRunInfo(id);
43+
return info?.taskId === CALENDAR_SYNC_TASK_ID;
44+
});
45+
}, [manager, runningTaskRunIds]);
46+
47+
const scheduledNextTimestamp = useMemo(() => {
48+
if (!manager) return null;
49+
for (const id of scheduledTaskRunIds) {
50+
const info = manager.getTaskRunInfo(id);
51+
if (info?.taskId === CALENDAR_SYNC_TASK_ID) {
52+
return info.nextTimestamp;
53+
}
54+
}
55+
return null;
56+
}, [manager, scheduledTaskRunIds]);
57+
58+
// Track when task completes (transitions from running to not running)
59+
useEffect(() => {
60+
if (
61+
!isRunning &&
62+
runningTaskRunIds.length === 0 &&
63+
lastCompletedAt === null
64+
) {
65+
// On initial mount, estimate last completion based on when next run is scheduled
66+
if (scheduledNextTimestamp) {
67+
const estimatedLastCompletion =
68+
scheduledNextTimestamp - CALENDAR_SYNC_INTERVAL;
69+
if (estimatedLastCompletion > 0) {
70+
setLastCompletedAt(estimatedLastCompletion);
71+
}
72+
}
73+
}
74+
}, [
75+
isRunning,
76+
runningTaskRunIds.length,
77+
lastCompletedAt,
78+
scheduledNextTimestamp,
79+
]);
80+
81+
// When task finishes running, update lastCompletedAt
82+
const wasRunningRef = useMemo(() => ({ current: isRunning }), []);
83+
useEffect(() => {
84+
if (wasRunningRef.current && !isRunning) {
85+
setLastCompletedAt(Date.now());
86+
}
87+
wasRunningRef.current = isRunning;
88+
}, [isRunning, wasRunningRef]);
89+
90+
// Update countdown timer
91+
useEffect(() => {
92+
const updateNextRunIn = () => {
93+
if (scheduledNextTimestamp) {
94+
const remaining = Math.max(
95+
0,
96+
Math.floor((scheduledNextTimestamp - Date.now()) / 1000),
97+
);
98+
setNextRunIn(remaining);
99+
} else if (lastCompletedAt) {
100+
const nextRun = lastCompletedAt + CALENDAR_SYNC_INTERVAL;
101+
const remaining = Math.max(
102+
0,
103+
Math.floor((nextRun - Date.now()) / 1000),
104+
);
105+
setNextRunIn(remaining);
106+
} else {
107+
setNextRunIn(null);
108+
}
109+
};
110+
111+
updateNextRunIn();
112+
const intervalId = setInterval(updateNextRunIn, 1000);
113+
return () => clearInterval(intervalId);
114+
}, [scheduledNextTimestamp, lastCompletedAt]);
32115

33116
const handleRefetch = useCallback(() => {
34-
const taskRunId = scheduleTaskRun();
35-
setCurrentTaskRunId(taskRunId);
117+
scheduleTaskRun();
36118
}, [scheduleTaskRun]);
37119

120+
const getStatusText = () => {
121+
if (isRunning) {
122+
return "Syncing...";
123+
}
124+
if (nextRunIn !== null && nextRunIn > 0) {
125+
return `Next sync in ${nextRunIn}s`;
126+
}
127+
return "Syncs every minute automatically";
128+
};
129+
38130
if (selectedCount === 0) {
39131
return null;
40132
}
@@ -45,9 +137,7 @@ export function CalendarStatus() {
45137
<span className="text-sm font-medium">
46138
{selectedCount} calendar{selectedCount !== 1 ? "s" : ""} selected
47139
</span>
48-
<span className="text-xs text-neutral-500">
49-
{isRunning ? "Syncing..." : "Syncs every minute automatically"}
50-
</span>
140+
<span className="text-xs text-neutral-500">{getStatusText()}</span>
51141
</div>
52142
<Button
53143
variant="outline"

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import type { Queries } from "tinybase/with-schemas";
22
import { useScheduleTaskRun, useSetTask } from "tinytick/ui-react";
33

44
import {
5+
CALENDAR_SYNC_INTERVAL,
56
CALENDAR_SYNC_TASK_ID,
67
syncCalendarEvents,
78
} from "../services/apple-calendar";
89
import * as main from "../store/tinybase/main";
910

10-
const CALENDAR_SYNC_INTERVAL = 60 * 1000; // 60 sec
11-
1211
export function TaskManager() {
1312
const store = main.UI.useStore(main.STORE_ID);
1413
const queries = main.UI.useQueries(main.STORE_ID);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { fetchExistingEvents, fetchIncomingEvents } from "./fetch";
66
import { execute, sync } from "./process";
77

88
export const CALENDAR_SYNC_TASK_ID = "calendarSync";
9+
export const CALENDAR_SYNC_INTERVAL = 60 * 1000; // 60 sec
910

1011
export async function syncCalendarEvents(
1112
store: Store,

0 commit comments

Comments
 (0)