diff --git a/src/src/components/Contexts/AuthContext.tsx b/src/src/components/Contexts/AuthContext.tsx index 8fc2d3e..0f0d663 100644 --- a/src/src/components/Contexts/AuthContext.tsx +++ b/src/src/components/Contexts/AuthContext.tsx @@ -1,4 +1,5 @@ import React from 'react'; + import { sendRequest } from '../Background/messages'; type Authenticated = { @@ -19,10 +20,10 @@ export const unsafeGetToken = () => unsafeToken; export function AuthenticationProvider({ children, }: { - children: React.ReactNode; + readonly children: React.ReactNode; }): JSX.Element { const handleAuthenticate = React.useCallback( - (interactive = true) => + async (interactive = true) => sendRequest('Authenticate', { interactive }) .then(({ token }) => { if (typeof token === 'string') { diff --git a/src/src/components/EventsStore/index.ts b/src/src/components/EventsStore/index.ts index 71a82e1..bac71bf 100644 --- a/src/src/components/EventsStore/index.ts +++ b/src/src/components/EventsStore/index.ts @@ -56,7 +56,7 @@ export function useEvents( endDate: Date | undefined ): EventsStore | undefined { const eventsStore = React.useRef({}); - // Clean temporary cache when overlay is closed + // Clear temporary cache when overlay is closed const clearCache = startDate === undefined || endDate === undefined; if (clearCache) eventsStore.current = {}; const calendars = React.useContext(CalendarsContext); @@ -145,14 +145,7 @@ export function useEvents( }) ); return extractData(eventsStore.current, calendars, startDate, endDate); - }, [ - eventsStore, - calendars, - startDate, - endDate, - ignoreAllDayEvents, - virtualCalendars, - ]), + }, [calendars, startDate, endDate, ignoreAllDayEvents, virtualCalendars]), false ); return durations; @@ -331,7 +324,8 @@ function extractData( const totals: R = Object.fromEntries( daysBetween.map((date) => [date, blankHours()]) ); - const entries = Object.entries(eventsStore[id]) + // "eventsStore" won't have an entry for current calendar if fetching failed + const entries = Object.entries(eventsStore?.[id] ?? {}) .map(([virtualCalendar, dates]) => { let categoryTotal = 0; return [ diff --git a/src/src/components/PowerTools/GhostEvents.tsx b/src/src/components/PowerTools/GhostEvents.tsx index 3e15199..c9d5fa3 100644 --- a/src/src/components/PowerTools/GhostEvents.tsx +++ b/src/src/components/PowerTools/GhostEvents.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { useSafeStorage } from '../../hooks/useStorage'; import { listen } from '../../utils/events'; import { f } from '../../utils/functools'; -import { debounce } from '../../utils/utils'; import { findMainContainer } from '../Molecules/Portal'; import { usePref } from '../Preferences/usePref'; +import { usePageListener } from './PageListener'; export function GhostEvents(): null { const [ghostEvents = [], setGhostEvents] = useSafeStorage( @@ -37,7 +37,6 @@ export function GhostEvents(): null { () => findMainContainer()?.parentElement ?? undefined, [] ); - const doGhosting = React.useCallback( (): void => mainContainer === undefined @@ -51,6 +50,7 @@ export function GhostEvents(): null { ), [mainContainer] ); + usePageListener(mainContainer, doGhosting); const ghostEventsRef = React.useRef(new Set()); React.useEffect(() => { @@ -58,22 +58,13 @@ export function GhostEvents(): null { doGhosting(); }, [ghostEvents, doGhosting]); - // Listen for DOM changes - React.useEffect(() => { - if (mainContainer === undefined) return undefined; - const config = { childList: true, subtree: true }; - const observer = new MutationObserver(debounce(doGhosting, 60)); - observer.observe(mainContainer, config); - return (): void => observer.disconnect(); - }, [mainContainer, doGhosting]); - // Listen for key press React.useEffect( () => - ghostEventShortcut === 'none' + ghostEventShortcut === 'none' || mainContainer === undefined ? undefined : listen( - document.body, + mainContainer, 'click', ({ shiftKey, ctrlKey, metaKey, target }) => { const keys = { @@ -94,7 +85,7 @@ export function GhostEvents(): null { setGhostEvents(f.unique([...ghostEvents, eventName])); } ), - [ghostEventShortcut, ghostEvents, setGhostEvents] + [mainContainer, ghostEventShortcut, ghostEvents, setGhostEvents] ); return null; diff --git a/src/src/components/PowerTools/PageListener.tsx b/src/src/components/PowerTools/PageListener.tsx new file mode 100644 index 0000000..90e591d --- /dev/null +++ b/src/src/components/PowerTools/PageListener.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { throttle } from '../../utils/utils'; + +/** + * Listen for the changes to events displayed on the page and call a callback + * when changes are detected. + */ +export function usePageListener( + mainContainer: HTMLElement | undefined, + callback: () => void +): void { + // Listen for DOM changes + React.useEffect(() => { + if (mainContainer === undefined) return undefined; + const config = { childList: true, subtree: true }; + const observer = new MutationObserver(throttle(callback, 60)); + observer.observe(mainContainer, config); + return (): void => observer.disconnect(); + }, [mainContainer, callback]); +} diff --git a/src/src/utils/utils.ts b/src/src/utils/utils.ts index 31d9729..3b43f9d 100644 --- a/src/src/utils/utils.ts +++ b/src/src/utils/utils.ts @@ -3,8 +3,8 @@ * * @module */ -import { IR, RA } from './types'; import { f } from './functools'; +import type { IR, RA } from './types'; export const capitalize = (string: T): Capitalize => (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize; @@ -26,6 +26,7 @@ export const sortFunction = else if (typeof leftValue === 'string' && typeof rightValue === 'string') return leftValue.localeCompare(rightValue) as -1 | 0 | 1; // Treat null and undefined as the same + // eslint-disable-next-line eqeqeq else if (leftValue == rightValue) return 0; return (leftValue ?? '') > (rightValue ?? '') ? 1 : -1; }; @@ -186,3 +187,21 @@ export function debounce(callback: () => void, timeout: number): () => void { timer = setTimeout(callback, timeout); }; } + +/** + * Based on simplified version of Underscore.js's throttle function + */ +export function throttle(callback: () => void, wait: number): () => void { + let timeout: ReturnType; + let previous = 0; + + return (): void => { + const time = Date.now(); + const remaining = wait - (time - previous); + if (remaining <= 0 || remaining > wait) { + clearTimeout(timeout); + previous = time; + callback(); + } + }; +}