diff --git a/package.json b/package.json index a63756c..bc54dc3 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ }, "license": "MIT", "dependencies": { - "@logseq/libs": "0.0.6", + "@logseq/libs": "0.0.17", "@types/uuid": "^8.3.4", "date-fns": "^2.28.0", "react": "^18.1.0", - "react-calendar-heatmap": "^1.8.1", + "react-calendar-heatmap": "^1.10.0", "react-dom": "^18.1.0", "react-suspense-fetch": "^0.6.0", "react-tooltip": "^4.2.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7452d85..cd11972 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,7 +1,7 @@ lockfileVersion: 5.4 specifiers: - '@logseq/libs': 0.0.6 + '@logseq/libs': 0.0.17 '@semantic-release/changelog': 6.0.2 '@semantic-release/exec': 6.0.3 '@semantic-release/git': 10.0.1 @@ -19,7 +19,7 @@ specifiers: eslint: 8.33.0 eslint-plugin-react: 7.32.2 react: ^18.1.0 - react-calendar-heatmap: ^1.8.1 + react-calendar-heatmap: ^1.10.0 react-dom: ^18.1.0 react-error-boundary: ^3.1.4 react-suspense-fetch: ^0.6.0 @@ -33,11 +33,11 @@ specifiers: windicss: 3.5.6 dependencies: - '@logseq/libs': 0.0.6 + '@logseq/libs': 0.0.17 '@types/uuid': 8.3.4 date-fns: 2.28.0 react: 18.1.0 - react-calendar-heatmap: 1.8.1_react@18.1.0 + react-calendar-heatmap: 1.10.0_react@18.1.0 react-dom: 18.1.0_react@18.1.0 react-suspense-fetch: 0.6.0 react-tooltip: 4.2.21_ef5jwxihqo6n7gxfmzogljlgcm @@ -422,12 +422,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true - /@logseq/libs/0.0.6: - resolution: {integrity: sha512-0Fv+Wdno5m0EA9xW6tRRvoKNPAF5nznpi6A1bBjh8aq4XMt7rGvhHbsH+u84/nswCYlce3t2Q/ei8CRK8iWbdw==} + /@logseq/libs/0.0.17: + resolution: {integrity: sha512-SkzzAaocmrgeHYrCOaRyEqzPOxw3d0qVEZSrt9qVvXE4tuEgbvEHR8tzI1N5RjgAv+PDWuGPiP7/mhcXHpINEw==} dependencies: - csstype: 3.0.8 - debug: 4.3.1 - dompurify: 2.3.1 + csstype: 3.1.0 + debug: 4.3.4 + deepmerge: 4.3.1 + dompurify: 2.3.8 eventemitter3: 4.0.7 fast-deep-equal: 3.1.3 lodash-es: 4.17.21 @@ -1377,10 +1378,6 @@ packages: source-map: 0.6.1 dev: false - /csstype/3.0.8: - resolution: {integrity: sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==} - dev: false - /csstype/3.1.0: resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==} @@ -1393,18 +1390,6 @@ packages: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} dev: true - /debug/4.3.1: - resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: false - /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -1415,7 +1400,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /decamelize-keys/1.1.0: resolution: {integrity: sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==} @@ -1439,6 +1423,11 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deepmerge/4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: false + /define-properties/1.1.4: resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} engines: {node: '>= 0.4'} @@ -1486,8 +1475,8 @@ packages: esutils: 2.0.3 dev: true - /dompurify/2.3.1: - resolution: {integrity: sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw==} + /dompurify/2.3.8: + resolution: {integrity: sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==} dev: false /dot-case/3.0.4: @@ -3311,7 +3300,7 @@ packages: dev: true /path/0.12.7: - resolution: {integrity: sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=} + resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} dependencies: process: 0.11.10 util: 0.10.4 @@ -3358,7 +3347,7 @@ packages: dev: true /process/0.11.10: - resolution: {integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI=} + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} dev: false @@ -3402,10 +3391,10 @@ packages: strip-json-comments: 2.0.1 dev: true - /react-calendar-heatmap/1.8.1_react@18.1.0: - resolution: {integrity: sha512-4Hbq/pDMJoCPzZnyIWFfHgokLlLXzKyGsDcMgNhYpi7zcKHcvsK9soLEPvhW2dBBqgDrQOSp/uG4wtifaDg4eQ==} + /react-calendar-heatmap/1.10.0_react@18.1.0: + resolution: {integrity: sha512-e5vcrzMWzKIF710egr1FpjWyuDEFeZm39nvV25muc8Wtqqi8iDOfqREELeQ9Wouqf9hhj939gq0i+iAxo7KdSw==} peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16.0.0 + react: '>=0.14.0' dependencies: memoize-one: 5.2.1 prop-types: 15.8.1 diff --git a/src/Heatmap.tsx b/src/Heatmap.tsx index c542f0c..7ec144f 100644 --- a/src/Heatmap.tsx +++ b/src/Heatmap.tsx @@ -10,13 +10,12 @@ import { ErrorBoundary, FallbackProps } from "react-error-boundary"; import * as React from "react"; import CalendarHeatmap from "react-calendar-heatmap"; import ReactTooltip from "react-tooltip"; -import { useMountedState, useWindowSize } from "react-use"; +import { useMountedState } from "react-use"; import "./Heatmap.css"; import { formatAsDashed, formatAsLocale, formatAsParam, - triggerIconName, parseJournalDate, useCurrentJournalDate, } from "./utils"; @@ -36,41 +35,61 @@ const useActivities = (startDate: string, endDate: string) => { const currentJournalDate = useCurrentJournalDate(); const [rawValue, setRawValue] = React.useState([]); + const [queryError, setQueryError] = React.useState(null); + + React.useEffect(() => { + let cancelled = false; - React.useLayoutEffect(() => { (async () => { - const date0 = new Date(startDate); - const date1 = new Date(endDate); + try { + const date0 = new Date(startDate); + const date1 = new Date(endDate); - const res: any[] = await logseq.DB.datascriptQuery(` - [:find (pull ?p [*]) (count ?b) - :where - [?b :block/page ?p] - [?p :block/journal? true] - [?p :block/journal-day ?d] - [?b :block/content ?c] - [(clojure.string/blank? ?c) ?empty] - [(not ?empty)] - [(>= ?d ${formatAsParam(date0)})] - [(<= ?d ${formatAsParam(date1)})]] - `); + // Return only scalar values. DB graphs expose pulled entity keys with + // namespaces (for example `block/journal-day`), so reading `name` and + // `journal-day` from a pulled object is both version-sensitive and much + // more expensive than asking Datascript for the values directly. + const res: any[] = await logseq.DB.datascriptQuery(` + [:find ?d ?name (count ?b) + :where + [?p :block/journal-day ?d] + [(>= ?d ${formatAsParam(date0)})] + [(<= ?d ${formatAsParam(date1)})] + [?p :block/name ?name] + [?b :block/page ?p] + [?b :block/title ?c] + [(clojure.string/blank? ?c) ?empty] + [(not ?empty)]] + `); - if (isMounted()) { - setRawValue(res); + if (!cancelled && isMounted()) { + setQueryError(null); + setRawValue(res); + } + } catch (error) { + if (!cancelled && isMounted()) { + setQueryError( + error instanceof Error ? error : new Error(String(error)) + ); + } } })(); + + return () => { + cancelled = true; + }; }, [startDate, endDate]); - return React.useMemo(() => { + const activities = React.useMemo(() => { const date0 = new Date(startDate); const date1 = new Date(endDate); const mapping = Object.fromEntries( - rawValue.map(([page, count]: any[]) => { - const date = parseJournalDate(page["journal-day"]); + rawValue.map(([journalDay, pageName, count]: any[]) => { + const date = parseJournalDate(journalDay); const datum = { count: count ?? 0, date: formatAsDashed(date), - originalName: page["original-name"] as string, + originalName: pageName as string, }; return [datum.date, datum]; }) @@ -101,6 +120,12 @@ const useActivities = (startDate: string, endDate: string) => { } return newValues; }, [rawValue, currentJournalDate]); + + if (queryError) { + throw queryError; + } + + return activities; }; type Datum = { @@ -246,33 +271,14 @@ const DateRange = ({ return null; }; -function useIconPosition() { - const windowSize = useWindowSize(); - return React.useMemo(() => { - let right = windowSize.width - 10; - let bottom = 20; - if (top?.document) { - const iconRect = top?.document - .querySelector(`.${triggerIconName}`) - ?.getBoundingClientRect(); - if (iconRect) { - right = iconRect.right; - bottom = iconRect.bottom; - } - } - return { right, bottom }; - }, [windowSize]); -} - export const Heatmap = React.forwardRef(({}, ref) => { const today = formatAsDashed(new Date()); const [range, setRange] = React.useState<[string, string] | null>(null); - const { bottom, right } = useIconPosition(); return (
diff --git a/src/utils.ts b/src/utils.ts index b502dec..2861a75 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -63,17 +63,29 @@ export const useThemeMode = () => { const isMounted = useMountedState(); const [mode, setMode] = React.useState<"dark" | "light">("light"); React.useEffect(() => { - setMode( - (top?.document - .querySelector("html") - ?.getAttribute("data-theme") as typeof mode) ?? - (matchMedia("prefers-color-scheme: dark").matches ? "dark" : "light") - ); - logseq.App.onThemeModeChanged((s) => { + let cancelled = false; + + logseq.App.getUserConfigs().then((config) => { + const configuredMode = config?.preferredThemeMode; + if ( + !cancelled && + isMounted() && + (configuredMode === "dark" || configuredMode === "light") + ) { + setMode(configuredMode); + } + }); + + const unsubscribe = logseq.App.onThemeModeChanged((s) => { if (isMounted()) { setMode(s.mode); } }); + + return () => { + cancelled = true; + unsubscribe?.(); + }; }, [isMounted]); return mode;