11import { RefreshCwIcon } from "lucide-react" ;
2- import { useCallback , useMemo , useState } from "react" ;
2+ import { useCallback , useEffect , useMemo , useState } from "react" ;
33import {
4+ useManager ,
5+ useRunningTaskRunIds ,
6+ useScheduledTaskRunIds ,
47 useScheduleTaskRunCallback ,
5- useTaskRunRunning ,
68} from "tinytick/ui-react" ;
79
810import { Button } from "@hypr/ui/components/ui/button" ;
911import { 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" ;
1217import * as main from "../../../store/tinybase/main" ;
1318
1419export 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"
0 commit comments