Skip to content

Commit 53cd280

Browse files
committed
b26 apply the new onViewWillDisappear and onViewDidAppear triggers and improve handling of 'midnight rollover'.
1 parent 9c62af0 commit 53cd280

5 files changed

Lines changed: 155 additions & 15 deletions

File tree

‎jgclark.Dashboard/CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ For more details see the [plugin's documentation](https://github.com/NotePlan/pl
88
- TODO: fix long-standing layout bug where some tooltips were getting clipped
99
- TODO: fix isNoteFromAllowedFolder() for teamspace or possibly 2025-W21.md
1010
-->
11+
## [2.4.0.b26] 2026-04-16
12+
- added support for new onViewWillDisappear and onViewDidAppear triggers -- to stop most timers, and refresh, respectively.
13+
- perf: Date-rollover refresh is now independent of idle auto-update. When the local calendar date changes while Dashboard is open, it refreshes enabled sections once per new day (after any open dialogs/dropdowns close), even if automatic idle refresh is disabled.
14+
1115
## [2.4.0.b25] 2026-04-15
1216
- removed the "Info" section from being available. Keeping code as it may well be useful as the basis for something else in future.
1317
- dev: Add new type of congrats message if all the Wins have been completed.

‎jgclark.Dashboard/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"plugin.description": "A Dashboard for NotePlan, that in one place shows:\n- a compact list of open tasks and checklists from today's note\n- scheduled open tasks and checklists from other notes.\n- similarly for yesterday's note, tomorrow's note, and the weekly, monthly and quarterly notes too (if used)\n- all overdue tasks\n- all open tasks and checklists that contain particular @tags or #mentions of your choosing\n- the next notes ready to review (if you use the 'Projects and Reviews' plugin).\nIt includes many other ways of speeding up managing your tasks: see the website for more details.",
99
"plugin.author": "@jgclark",
1010
"plugin.comment": "TODO: On full release, change minAppVersion down to 3.7?",
11-
"plugin.version": "2.4.0.b24",
11+
"plugin.version": "2.4.0.b26",
1212
"plugin.releaseStatus": "beta",
1313
"plugin.hidden": false,
1414
"plugin.lastUpdateInfo": "2.4.0: new 'Spaces to Include' setting which controls which (Team)Spaces you wish to include, plus whether or not to include the Private 'Space' (all notes not in a Space)\n2.3.3: new 'Year' section available.\n2.3.2: fix display when there are no priority items shown.\n2.3.1: fix for possible loss of settings error when upgrading.\n2.3.0: Support for NotePlan (Team)Spaces. Can re-order display of Sections.New '/backupSettings' command. Added 'noteTags' feature. Speeded up Tag/Mention sections. Layout improvements. Lots of other small fixes and improvements.\n2.2.1: Add new sorting option for Tag and Overdue sections.\n2.2.0: Add 'Search' section. New keyboard shortcuts. Plus many small improvements, bug fixes and performance improvements. See documentation for details.\n2.1.10: More move-under-heading options. Bug fixes and performance improvements.\n2.1.9: performance improvements and better UI for iPhone users.\n2.1.8: various fixes and small improvements.\n2.1.7: various fixes and small improvements.\n2.1.6: allow all current timeblocks to be shown, not just the first. Add new @repeat()s if using the extended syntax from the Repeat Extensions plugin. Bug fixes.\n2.1.5: fixes to time blocks and scheduling items.\n2.1.4: fix to Interactive Processing, and Edit All Perspectives dialog now shows unsaved changes.",

‎jgclark.Dashboard/src/react/components/Dashboard.jsx‎

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//--------------------------------------------------------------------------
33
// Dashboard React component to aggregate data and layout for the dashboard
44
// Called by WebView component.
5-
// Last updated for 2026-04-13 for v2.4.0.b23, @jgclark
5+
// Last updated for 2026-04-16 for v2.4.0.b25, @jgclark
66
//--------------------------------------------------------------------------
77

88
//--------------------------------------------------------------------------
@@ -11,11 +11,12 @@
1111
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1212
import useRefreshTimer from '../customHooks/useRefreshTimer.jsx'
1313
import useWatchForResizes from '../customHooks/useWatchForResizes.jsx'
14+
import useMidnightRollover from '../customHooks/useMidnightRollover.jsx'
1415
import { dontDedupeSectionCodes, sectionPriority, defaultSectionDisplayOrder } from '../../constants.js'
1516
import { copyUpdatedSectionItemData } from '../../dataGeneration.js'
1617
import { findSectionItems } from '../../dashboardHelpers.js'
1718
import { dashboardSettingDefs, dashboardFilterDefs } from '../../dashboardSettings.js'
18-
import type { TSection, TActionButton } from '../../types.js'
19+
import type { TActionButton } from '../../types.js'
1920
import { useAppContext } from './AppContext.jsx'
2021
import Dialog from './Dialog.jsx'
2122
import { getSectionsWithoutDuplicateLines, injectSyntheticWinsSection, countTotalVisibleSectionItems, sortSections, showSectionSettingItems } from './Section/sectionHelpers.js'
@@ -82,7 +83,7 @@ const Dashboard = ({ pluginData }: Props): React$Node => {
8283
}
8384
// 5s hack timer to work around cache not being reliable (only runs for users, not DEVs, and not in Demo mode)
8485
const shortDelayTimerIsOn = logSettings._logLevel !== 'DEV' && !pluginData.demoMode
85-
const { refreshTimer } = useRefreshTimer({ maxDelay: 5000, enabled: shortDelayTimerIsOn })
86+
const { refreshTimer, cancelRefreshTimer } = useRefreshTimer({ maxDelay: 5000, enabled: shortDelayTimerIsOn })
8687

8788
//----------------------------------------------------------------------
8889
// Refs
@@ -93,6 +94,7 @@ const Dashboard = ({ pluginData }: Props): React$Node => {
9394
// State
9495
//----------------------------------------------------------------------
9596
const [dropdownMenuOpen, setDropdownMenuOpen] = useState(false)
97+
const [isViewVisible, setIsViewVisible] = useState(true)
9698

9799
// Order the display of sections, and count the total number of items to show
98100
const { sections, totalSectionItems } = useMemo(() => {
@@ -137,7 +139,7 @@ const Dashboard = ({ pluginData }: Props): React$Node => {
137139
)
138140

139141
// Disable auto-update in Demo mode
140-
const autoUpdateEnabled = parseInt(dashboardSettings?.autoUpdateAfterIdleTime || '0') > 0 && !pluginData.demoMode
142+
const autoUpdateEnabled = parseInt(dashboardSettings?.autoUpdateAfterIdleTime || '0') > 0 && !pluginData.demoMode && isViewVisible
141143

142144
// Pause IdleTimer while any of the edit/settings dialogs or dropdown menus are open (DialogForTaskItems, DialogForProjectItems, Dashboard Settings, Header dropdowns)
143145
const dialogsOpen =
@@ -299,6 +301,43 @@ const Dashboard = ({ pluginData }: Props): React$Node => {
299301
sendActionToPlugin(actionType, { actionType }, 'Auto-Refresh time!', true)
300302
}
301303

304+
// Trigger a once-per-day refresh of enabled sections, independent of idle auto-update settings.
305+
const handleDateRollover = useCallback(() => {
306+
const actionType = 'refreshEnabledSections'
307+
logDebug('Dashboard', 'Date rollover detected; sending refreshEnabledSections')
308+
sendActionToPlugin(actionType, { actionType }, 'Date rollover detected; sending refreshEnabledSections', true)
309+
}, [sendActionToPlugin])
310+
311+
const refreshVisibleDashboard = useCallback(() => {
312+
logDebug('Dashboard', 'onViewDidAppear event handler called for Dashboard window')
313+
setIsViewVisible(true)
314+
const actionType = 'refreshEnabledSections'
315+
sendActionToPlugin(actionType, { actionType }, 'Dashboard window became visible; sending action refreshEnabledSections', true)
316+
}, [sendActionToPlugin])
317+
318+
const pauseHiddenDashboard = useCallback(() => {
319+
logDebug('Dashboard', 'onViewWillDisappear event handler called for Dashboard window')
320+
setIsViewVisible(false)
321+
cancelRefreshTimer()
322+
}, [cancelRefreshTimer])
323+
324+
useEffect(() => {
325+
window.addEventListener('onViewDidAppear', refreshVisibleDashboard)
326+
window.addEventListener('onViewWillDisappear', pauseHiddenDashboard)
327+
328+
return () => {
329+
window.removeEventListener('onViewDidAppear', refreshVisibleDashboard)
330+
window.removeEventListener('onViewWillDisappear', pauseHiddenDashboard)
331+
}
332+
}, [pauseHiddenDashboard, refreshVisibleDashboard])
333+
334+
useMidnightRollover({
335+
enabled: !pluginData.demoMode,
336+
userIsInteracting: dialogsOpen,
337+
isViewVisible,
338+
onDateRollover: handleDateRollover,
339+
})
340+
302341
const hidePerspectivesTable = () => {
303342
setReactSettings((prevReactSettings) => ({ ...prevReactSettings, perspectivesTableVisible: false }))
304343
}
@@ -314,7 +353,11 @@ const Dashboard = ({ pluginData }: Props): React$Node => {
314353
<>
315354
<div style={dashboardContainerStyle} tabIndex={0} ref={containerRef} className={pluginData.platform ?? ''}>
316355
{autoUpdateEnabled && (
317-
<IdleTimer idleTime={parseInt(dashboardSettings?.autoUpdateAfterIdleTime ? dashboardSettings.autoUpdateAfterIdleTime : '15') * 60 * 1000} onIdleTimeout={autoRefresh} userIsInteracting={dialogsOpen} />
356+
<IdleTimer
357+
idleTime={parseInt(dashboardSettings?.autoUpdateAfterIdleTime ? dashboardSettings.autoUpdateAfterIdleTime : '15') * 60 * 1000}
358+
onIdleTimeout={autoRefresh}
359+
userIsInteracting={dialogsOpen}
360+
/>
318361
)}
319362
{/* Note: this is where I might want to put further periodic data generation functions: completed task counter etc. */}
320363
{reactSettings?.perspectivesTableVisible && (
@@ -338,7 +381,12 @@ const Dashboard = ({ pluginData }: Props): React$Node => {
338381
<NonModalSpinner textBelow="Switching perspectives" style={{ container: { color: 'var(--tint-color)', textAlign: 'center', marginTop: '0.6rem', marginBottom: '0rem' } }} />
339382
)}
340383
{pluginData?.logSettings?._logLevel === 'DEV' && (
341-
<DebugPanel isVisible={showDebugPanel} getContext={getContext} testGroups={testGroups} defaultExpandedKeys={['Context Variables', 'perspectiveSettings']} />
384+
<DebugPanel
385+
isVisible={showDebugPanel}
386+
getContext={getContext}
387+
testGroups={(testGroups: any)}
388+
defaultExpandedKeys={['Context Variables', 'perspectiveSettings']}
389+
/>
342390
)}
343391
</div>
344392
{/* Note: the Tooltip Portal is deliberately outside the Dashboard Container, not that it seems to have any effect on the tooltip z-index issue. */}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// @flow
2+
//------------------------------------------------------------------------------
3+
// useMidnightRollover.jsx
4+
// Detect calendar date rollover while Dashboard is open and visible, and
5+
// trigger a callback once per new day, even if idle auto-refresh is disabled.
6+
// Last updated for 2026-04-16 for v2.4.0.b26, @Cursor
7+
//------------------------------------------------------------------------------
8+
9+
import { useEffect, useRef, useState } from 'react'
10+
import moment from 'moment/min/moment-with-locales'
11+
import { logDebug } from '@helpers/react/reactDev'
12+
13+
type UseMidnightRolloverOptions = {
14+
enabled: boolean,
15+
userIsInteracting: boolean,
16+
isViewVisible: boolean,
17+
onDateRollover: () => void,
18+
}
19+
20+
const TICK_INTERVAL_MS = 15000 // 15 seconds, same cadence as IdleTimer
21+
22+
/**
23+
* Hook that detects when the local calendar date changes while the Dashboard
24+
* view is open, and calls `onDateRollover` once per new date.
25+
*
26+
* - Independent of idle auto-refresh settings.
27+
* - Safe across sleep/wake (relies on date change, not exact 00:00).
28+
* - Defers the callback until the view is visible and not interacting.
29+
*/
30+
export default function useMidnightRollover(options: UseMidnightRolloverOptions): void {
31+
const { enabled, userIsInteracting, isViewVisible, onDateRollover } = options
32+
33+
const lastProcessedDateRef = useRef<?string>(null)
34+
const [pendingRolloverDate, setPendingRolloverDate] = useState<?string>(null)
35+
36+
// Track calendar date changes while enabled
37+
useEffect(() => {
38+
if (!enabled) {
39+
return
40+
}
41+
42+
const tick = () => {
43+
const currentDate = moment().format('YYYY-MM-DD')
44+
45+
if (lastProcessedDateRef.current == null) {
46+
// First run: initialise but don't fire
47+
lastProcessedDateRef.current = currentDate
48+
logDebug('useMidnightRollover', `First run: Initialising lastProcessedDateRef to ${currentDate}`)
49+
return
50+
}
51+
52+
if (currentDate !== lastProcessedDateRef.current) {
53+
lastProcessedDateRef.current = currentDate
54+
setPendingRolloverDate(currentDate)
55+
logDebug('useMidnightRollover', `Detected new calendar date ${currentDate}; queuing rollover`)
56+
}
57+
}
58+
59+
const intervalId = setInterval(tick, TICK_INTERVAL_MS)
60+
61+
return () => {
62+
clearInterval(intervalId)
63+
}
64+
}, [enabled])
65+
66+
// When a rollover is pending, wait until the view is visible and not interacting
67+
useEffect(() => {
68+
if (!pendingRolloverDate) {
69+
return
70+
}
71+
if (!enabled) {
72+
return
73+
}
74+
if (!isViewVisible || userIsInteracting) {
75+
return
76+
}
77+
78+
logDebug('useMidnightRollover', `Processing queued date rollover for ${String(pendingRolloverDate)} (view visible, not interacting)`)
79+
setPendingRolloverDate(null)
80+
onDateRollover()
81+
}, [enabled, isViewVisible, pendingRolloverDate, userIsInteracting, onDateRollover])
82+
}
83+

‎jgclark.Dashboard/src/react/customHooks/useRefreshTimer.jsx‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
// refreshTimer()
1010
//------------------------------------------------------------------------------
1111

12-
import { useState, useEffect } from 'react'
12+
import { useState, useEffect, useCallback } from 'react'
1313
import { useAppContext } from '../components/AppContext.jsx'
14-
import { logDebug } from '@helpers/react/reactDev.js'
14+
import { logDebug, logError, logInfo, logWarn } from '@helpers/react/reactDev.js'
1515

1616
/**
1717
* Options for the refresh timer hook.
@@ -30,6 +30,7 @@ type RefreshTimerOptions = {
3030
*/
3131
type RefreshTimerReturn = {
3232
refreshTimer: () => void,
33+
cancelRefreshTimer: () => void,
3334
}
3435

3536
/**
@@ -43,12 +44,19 @@ function useRefreshTimer(options: RefreshTimerOptions): RefreshTimerReturn {
4344
const [timerId, setTimerId] = useState <? TimeoutID > (null)
4445
const { sendActionToPlugin } = useAppContext()
4546

47+
const cancelRefreshTimer = useCallback((): void => {
48+
if (timerId) {
49+
clearTimeout(timerId)
50+
setTimerId(null)
51+
logInfo('useRefreshTimer', 'Cancelling previously set Timer ...')
52+
}
53+
}, [timerId])
54+
4655
useEffect(() => {
4756
return () => {
4857
// Clear the timer when the component unmounts
4958
if (timerId) {
5059
clearTimeout(timerId)
51-
logDebug('useRefreshTimer', 'Timer was set previously, resetting timer...')
5260
}
5361
}
5462
}, [timerId])
@@ -57,10 +65,7 @@ function useRefreshTimer(options: RefreshTimerOptions): RefreshTimerReturn {
5765
* Function to trigger refresh.
5866
*/
5967
const refreshTimer = (): void => {
60-
if (timerId) {
61-
// If a timer is already running, clear it
62-
clearTimeout(timerId)
63-
}
68+
cancelRefreshTimer()
6469
// Set a new timer with the maximum delay
6570
const newTimerId = setTimeout(() => {
6671
// Trigger the refresh action
@@ -75,7 +80,7 @@ function useRefreshTimer(options: RefreshTimerOptions): RefreshTimerReturn {
7580
setTimerId(newTimerId)
7681
}
7782

78-
return { refreshTimer }
83+
return { refreshTimer, cancelRefreshTimer }
7984
}
8085

8186
export default useRefreshTimer

0 commit comments

Comments
 (0)