Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 103 additions & 13 deletions apps/desktop/src/components/settings/calendar/status.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,132 @@
import { RefreshCwIcon } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
useManager,
useRunningTaskRunIds,
useScheduledTaskRunIds,
useScheduleTaskRunCallback,
useTaskRunRunning,
} from "tinytick/ui-react";

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

import { CALENDAR_SYNC_TASK_ID } from "../../../services/apple-calendar";
import {
CALENDAR_SYNC_INTERVAL,
CALENDAR_SYNC_TASK_ID,
} from "../../../services/apple-calendar";
import * as main from "../../../store/tinybase/main";

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

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

const [currentTaskRunId, setCurrentTaskRunId] = useState<string | undefined>(
undefined,
);

const scheduleTaskRun = useScheduleTaskRunCallback(
CALENDAR_SYNC_TASK_ID,
undefined,
0,
);

const isRunning = useTaskRunRunning(currentTaskRunId ?? "");
const runningTaskRunIds = useRunningTaskRunIds();
const scheduledTaskRunIds = useScheduledTaskRunIds();

const [lastCompletedAt, setLastCompletedAt] = useState<number | null>(null);
const [nextRunIn, setNextRunIn] = useState<number | null>(null);

const isRunning = useMemo(() => {
if (!manager) return false;
return runningTaskRunIds.some((id) => {
const info = manager.getTaskRunInfo(id);
return info?.taskId === CALENDAR_SYNC_TASK_ID;
});
}, [manager, runningTaskRunIds]);

const scheduledNextTimestamp = useMemo(() => {
if (!manager) return null;
for (const id of scheduledTaskRunIds) {
const info = manager.getTaskRunInfo(id);
if (info?.taskId === CALENDAR_SYNC_TASK_ID) {
return info.nextTimestamp;
}
}
return null;
}, [manager, scheduledTaskRunIds]);

// Track when task completes (transitions from running to not running)
useEffect(() => {
if (
!isRunning &&
runningTaskRunIds.length === 0 &&
lastCompletedAt === null
) {
// On initial mount, estimate last completion based on when next run is scheduled
if (scheduledNextTimestamp) {
const estimatedLastCompletion =
scheduledNextTimestamp - CALENDAR_SYNC_INTERVAL;
if (estimatedLastCompletion > 0) {
setLastCompletedAt(estimatedLastCompletion);
}
}
}
}, [
isRunning,
runningTaskRunIds.length,
lastCompletedAt,
scheduledNextTimestamp,
]);

// When task finishes running, update lastCompletedAt
const wasRunningRef = useMemo(() => ({ current: isRunning }), []);
useEffect(() => {
if (wasRunningRef.current && !isRunning) {
setLastCompletedAt(Date.now());
}
wasRunningRef.current = isRunning;
}, [isRunning, wasRunningRef]);

// Update countdown timer
useEffect(() => {
const updateNextRunIn = () => {
if (scheduledNextTimestamp) {
const remaining = Math.max(
0,
Math.floor((scheduledNextTimestamp - Date.now()) / 1000),
);
setNextRunIn(remaining);
} else if (lastCompletedAt) {
const nextRun = lastCompletedAt + CALENDAR_SYNC_INTERVAL;
const remaining = Math.max(
0,
Math.floor((nextRun - Date.now()) / 1000),
);
setNextRunIn(remaining);
} else {
setNextRunIn(null);
}
};

updateNextRunIn();
const intervalId = setInterval(updateNextRunIn, 1000);
return () => clearInterval(intervalId);
}, [scheduledNextTimestamp, lastCompletedAt]);

const handleRefetch = useCallback(() => {
const taskRunId = scheduleTaskRun();
setCurrentTaskRunId(taskRunId);
scheduleTaskRun();
}, [scheduleTaskRun]);

const getStatusText = () => {
if (isRunning) {
return "Syncing...";
}
if (nextRunIn !== null && nextRunIn > 0) {
return `Next sync in ${nextRunIn}s`;
}
return "Syncs every minute automatically";
};

if (selectedCount === 0) {
return null;
}
Expand All @@ -45,9 +137,7 @@ export function CalendarStatus() {
<span className="text-sm font-medium">
{selectedCount} calendar{selectedCount !== 1 ? "s" : ""} selected
</span>
<span className="text-xs text-neutral-500">
{isRunning ? "Syncing..." : "Syncs every minute automatically"}
</span>
<span className="text-xs text-neutral-500">{getStatusText()}</span>
</div>
<Button
variant="outline"
Expand Down
3 changes: 1 addition & 2 deletions apps/desktop/src/components/task-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import type { Queries } from "tinybase/with-schemas";
import { useScheduleTaskRun, useSetTask } from "tinytick/ui-react";

import {
CALENDAR_SYNC_INTERVAL,
CALENDAR_SYNC_TASK_ID,
syncCalendarEvents,
} from "../services/apple-calendar";
import * as main from "../store/tinybase/main";

const CALENDAR_SYNC_INTERVAL = 60 * 1000; // 60 sec

export function TaskManager() {
const store = main.UI.useStore(main.STORE_ID);
const queries = main.UI.useQueries(main.STORE_ID);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/services/apple-calendar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { fetchExistingEvents, fetchIncomingEvents } from "./fetch";
import { execute, sync } from "./process";

export const CALENDAR_SYNC_TASK_ID = "calendarSync";
export const CALENDAR_SYNC_INTERVAL = 60 * 1000; // 60 sec

export async function syncCalendarEvents(
store: Store,
Expand Down