diff --git a/apps/roam/src/components/DiscourseFloatingMenu.tsx b/apps/roam/src/components/DiscourseFloatingMenu.tsx index 3a7e337b9..a96c33f1b 100644 --- a/apps/roam/src/components/DiscourseFloatingMenu.tsx +++ b/apps/roam/src/components/DiscourseFloatingMenu.tsx @@ -13,7 +13,7 @@ import { import { FeedbackWidget } from "./BirdEatsBugs"; import { render as renderSettings } from "~/components/settings/Settings"; import posthog from "posthog-js"; -import { getPersonalSetting } from "./settings/utils/accessors"; +import { type SettingsSnapshot } from "./settings/utils/accessors"; import { PERSONAL_KEYS } from "./settings/utils/settingKeys"; type DiscourseFloatingMenuProps = { @@ -118,11 +118,7 @@ export const showDiscourseFloatingMenu = () => { export const installDiscourseFloatingMenu = ( onLoadArgs: OnloadArgs, - props: DiscourseFloatingMenuProps = { - position: "bottom-right", - theme: "bp3-light", - buttonTheme: "bp3-light", - }, + snapshot: SettingsSnapshot, ) => { let floatingMenuAnchor = document.getElementById(ANCHOR_ID); if (!floatingMenuAnchor) { @@ -130,14 +126,15 @@ export const installDiscourseFloatingMenu = ( floatingMenuAnchor.id = ANCHOR_ID; document.getElementById("app")?.appendChild(floatingMenuAnchor); } - if (getPersonalSetting([PERSONAL_KEYS.hideFeedbackButton])) { + if (snapshot.personalSettings[PERSONAL_KEYS.hideFeedbackButton]) { floatingMenuAnchor.classList.add("hidden"); } + // eslint-disable-next-line react/no-deprecated ReactDOM.render( , floatingMenuAnchor, @@ -148,6 +145,7 @@ export const removeDiscourseFloatingMenu = () => { const anchor = document.getElementById(ANCHOR_ID); if (anchor) { try { + // eslint-disable-next-line react/no-deprecated ReactDOM.unmountComponentAtNode(anchor); } catch (e) { // no-op: unmount best-effort diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 46ca39cc5..b5793707c 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -39,6 +39,7 @@ import { getPersonalSettings, setGlobalSetting, setPersonalSetting, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS, @@ -336,14 +337,16 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { // TODO(ENG-1471): Remove old-system merge when migration complete — just use accessor values directly. // See mergeGlobalSectionWithAccessor/mergePersonalSectionsWithAccessor for why the merge exists. -const buildConfig = (): LeftSidebarConfig => { +const buildConfig = (snapshot?: SettingsSnapshot): LeftSidebarConfig => { // Read VALUES from accessor (handles flag routing + mismatch detection) - const globalValues = getGlobalSetting([ - GLOBAL_KEYS.leftSidebar, - ]); - const personalValues = getPersonalSetting< - ReturnType[typeof PERSONAL_KEYS.leftSidebar] - >([PERSONAL_KEYS.leftSidebar]); + const globalValues = snapshot + ? snapshot.globalSettings[GLOBAL_KEYS.leftSidebar] + : getGlobalSetting([GLOBAL_KEYS.leftSidebar]); + const personalValues = snapshot + ? snapshot.personalSettings[PERSONAL_KEYS.leftSidebar] + : getPersonalSetting< + ReturnType[typeof PERSONAL_KEYS.leftSidebar] + >([PERSONAL_KEYS.leftSidebar]); // Read UIDs from old system (needed for fold CRUD during dual-write) const oldConfig = getCurrentLeftSidebarConfig(); @@ -364,8 +367,8 @@ const buildConfig = (): LeftSidebarConfig => { }; }; -export const useConfig = () => { - const [config, setConfig] = useState(() => buildConfig()); +export const useConfig = (initialSnapshot?: SettingsSnapshot) => { + const [config, setConfig] = useState(() => buildConfig(initialSnapshot)); useEffect(() => { const handleUpdate = () => { setConfig(buildConfig()); @@ -504,8 +507,14 @@ const FavoritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { ); }; -const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { - const { config } = useConfig(); +const LeftSidebarView = ({ + onloadArgs, + initialSnapshot, +}: { + onloadArgs: OnloadArgs; + initialSnapshot?: SettingsSnapshot; +}) => { + const { config } = useConfig(initialSnapshot); return ( <> @@ -610,10 +619,15 @@ const migrateFavorites = async () => { refreshConfigTree(); }; -export const mountLeftSidebar = async ( - wrapper: HTMLElement, - onloadArgs: OnloadArgs, -): Promise => { +export const mountLeftSidebar = async ({ + wrapper, + onloadArgs, + initialSnapshot, +}: { + wrapper: HTMLElement; + onloadArgs: OnloadArgs; + initialSnapshot?: SettingsSnapshot; +}): Promise => { if (!wrapper) return; const id = "dg-left-sidebar-root"; @@ -630,7 +644,14 @@ export const mountLeftSidebar = async ( } else { root.className = "starred-pages"; } - ReactDOM.render(, root); + // eslint-disable-next-line react/no-deprecated + ReactDOM.render( + , + root, + ); }; export default LeftSidebarView; diff --git a/apps/roam/src/components/settings/QueryPagesPanel.tsx b/apps/roam/src/components/settings/QueryPagesPanel.tsx index 49122bf5e..c6f792dca 100644 --- a/apps/roam/src/components/settings/QueryPagesPanel.tsx +++ b/apps/roam/src/components/settings/QueryPagesPanel.tsx @@ -5,6 +5,7 @@ import type { OnloadArgs } from "roamjs-components/types"; import { getPersonalSetting, setPersonalSetting, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS, @@ -13,11 +14,13 @@ import { // Legacy extensionAPI stored query-pages as string | string[] | Record. // Coerce to string[] for backward compatibility with old stored formats. -export const getQueryPages = (): string[] => { - const value = getPersonalSetting>([ - PERSONAL_KEYS.query, - QUERY_KEYS.queryPages, - ]); +export const getQueryPages = (snapshot?: SettingsSnapshot): string[] => { + const value: string[] | string | Record | undefined = snapshot + ? snapshot.personalSettings[PERSONAL_KEYS.query][QUERY_KEYS.queryPages] + : getPersonalSetting>([ + PERSONAL_KEYS.query, + QUERY_KEYS.queryPages, + ]); return typeof value === "string" ? [value] : Array.isArray(value) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 2eb2c0b2a..b23b466b3 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -863,8 +863,10 @@ export const setGlobalSetting = (keys: string[], value: json): void => { }); }; -export const getAllRelations = (): DiscourseRelation[] => { - const settings = getGlobalSettings(); +export const getAllRelations = ( + snapshot?: SettingsSnapshot, +): DiscourseRelation[] => { + const settings = snapshot ? snapshot.globalSettings : getGlobalSettings(); return Object.entries(settings.Relations).flatMap(([id, relation]) => relation.ifConditions.map((ifCondition) => ({ @@ -909,6 +911,63 @@ export const getPersonalSetting = ( return blockPropsValue as T | undefined; }; +export type SettingsSnapshot = { + featureFlags: FeatureFlags; + globalSettings: GlobalSettings; + personalSettings: PersonalSettings; +}; + +export const bulkReadSettings = (): SettingsSnapshot => { + const pageResult = window.roamAlphaAPI.pull( + "[{:block/children [:block/string :block/props]}]", + [":node/title", DG_BLOCK_PROP_SETTINGS_PAGE_TITLE], + ) as Record | null; + + const children = (pageResult?.[":block/children"] ?? []) as Record< + string, + json + >[]; + const personalKey = getPersonalSettingsKey(); + let featureFlagsProps: json = {}; + let globalProps: json = {}; + let personalProps: json = {}; + + for (const child of children) { + const text = child[":block/string"]; + if (typeof text !== "string") continue; + const rawBlockProps = child[":block/props"]; + const blockProps = + rawBlockProps && typeof rawBlockProps === "object" + ? normalizeProps(rawBlockProps) + : {}; + if (text === TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags) { + featureFlagsProps = blockProps; + } else if (text === TOP_LEVEL_BLOCK_PROP_KEYS.global) { + globalProps = blockProps; + } else if (text === personalKey) { + personalProps = blockProps; + } + } + + const featureFlags = FeatureFlagsSchema.parse(featureFlagsProps || {}); + + if (!featureFlags["Use new settings store"]) { + return { + featureFlags, + globalSettings: GlobalSettingsSchema.parse(readAllLegacyGlobalSettings()), + personalSettings: PersonalSettingsSchema.parse( + readAllLegacyPersonalSettings(), + ), + }; + } + + return { + featureFlags, + globalSettings: GlobalSettingsSchema.parse(globalProps || {}), + personalSettings: PersonalSettingsSchema.parse(personalProps || {}), + }; +}; + export const setPersonalSetting = (keys: string[], value: json): void => { if (keys.length === 0) { internalError({ diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index 07a15f1d6..3720f939e 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -336,6 +336,9 @@ export type InitSchemaResult = { nodePageUids: Record; }; +// On-demand dual-read comparison. Not called automatically on init — +// invoke from the console via window.dgDualReadLog() to inspect the legacy +// settings tree vs. the block-prop store. const logDualReadComparison = (): void => { if (!isNewSettingsStoreEnabled()) return; @@ -415,11 +418,6 @@ export const initSchema = async (): Promise => { await migrateGraphLevel(blockUids); const nodePageUids = await initDiscourseNodePages(); await migratePersonalSettings(blockUids); - try { - logDualReadComparison(); - } catch (e) { - console.warn("[DG Dual-Read] Comparison failed:", e); - } (window as unknown as Record).dgDualReadLog = logDualReadComparison; return { blockUids, nodePageUids }; diff --git a/apps/roam/src/components/settings/utils/migrateLegacyToBlockProps.ts b/apps/roam/src/components/settings/utils/migrateLegacyToBlockProps.ts index 8ca1fe496..0e0fa45c9 100644 --- a/apps/roam/src/components/settings/utils/migrateLegacyToBlockProps.ts +++ b/apps/roam/src/components/settings/utils/migrateLegacyToBlockProps.ts @@ -1,7 +1,6 @@ import getBlockProps from "~/utils/getBlockProps"; import type { json } from "~/utils/getBlockProps"; import setBlockProps from "~/utils/setBlockProps"; -import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import { createBlock } from "roamjs-components/writes"; import { getSetting, setSetting } from "~/utils/extensionSettings"; @@ -29,11 +28,8 @@ const GRAPH_MIGRATION_MARKER = "Block props migrated"; const PERSONAL_MIGRATION_MARKER = "dg-personal-settings-migrated"; const MAX_ERROR_CONTEXT_LENGTH = 5000; -const hasGraphMigrationMarker = (): boolean => - !!getBlockUidByTextOnPage({ - text: GRAPH_MIGRATION_MARKER, - title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, - }); +const hasGraphMigrationMarker = (blockMap: Record): boolean => + !!blockMap[GRAPH_MIGRATION_MARKER]; const isPropsValid = ( schema: z.ZodTypeAny, @@ -182,7 +178,7 @@ export const migrateGraphLevel = async ( return; } - if (hasGraphMigrationMarker()) { + if (hasGraphMigrationMarker(blockUids)) { console.log(`${LOG_PREFIX} graph-level: skipped (already migrated)`); return; } diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index ff8e95d9a..68d404ede 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -34,10 +34,7 @@ import { import { initPluginTimer } from "./utils/pluginTimer"; import { initPostHog } from "./utils/posthog"; import { initSchema } from "./components/settings/utils/init"; -import { - getFeatureFlag, - getPersonalSetting, -} from "./components/settings/utils/accessors"; +import { bulkReadSettings } from "./components/settings/utils/accessors"; import { PERSONAL_KEYS } from "./components/settings/utils/settingKeys"; import { setupPullWatchOnSettingsPage } from "./components/settings/utils/pullWatchers"; import { @@ -49,12 +46,11 @@ import { mountLeftSidebar } from "./components/LeftSidebarView"; export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; export default runExtension(async (onloadArgs) => { - const isEncrypted = window.roamAlphaAPI.graph.isEncrypted; - const isOffline = window.roamAlphaAPI.graph.type === "offline"; - const disallowDiagnostics = getPersonalSetting([ - PERSONAL_KEYS.disableProductDiagnostics, - ]); - if (!isEncrypted && !isOffline && !disallowDiagnostics) { + const pluginLoadStart = performance.now(); + + const settings = bulkReadSettings(); + + if (!settings.personalSettings[PERSONAL_KEYS.disableProductDiagnostics]) { initPostHog(); } @@ -81,14 +77,16 @@ export default runExtension(async (onloadArgs) => { initPluginTimer(); - await initializeDiscourseNodes(); - refreshConfigTree(); + await initializeDiscourseNodes(settings); + + refreshConfigTree(settings); addGraphViewNodeStyling(); registerCommandPaletteCommands(onloadArgs); createSettingsPanel(onloadArgs); registerSmartBlock(onloadArgs); - setInitialQueryPages(onloadArgs); + + setInitialQueryPages(onloadArgs, settings); const style = addStyle(styles); const discourseGraphStyle = addStyle(discourseGraphStyles); @@ -96,16 +94,18 @@ export default runExtension(async (onloadArgs) => { const discourseFloatingMenuStyle = addStyle(discourseFloatingMenuStyles); // Add streamline styling only if enabled - const isStreamlineStylingEnabled = getPersonalSetting([ - PERSONAL_KEYS.streamlineStyling, - ]); + const isStreamlineStylingEnabled = + settings.personalSettings[PERSONAL_KEYS.streamlineStyling]; let streamlineStyleElement: HTMLStyleElement | null = null; if (isStreamlineStylingEnabled) { streamlineStyleElement = addStyle(streamlineStyling); streamlineStyleElement.id = "streamline-styling"; } - const { observers, listeners, cleanups } = initObservers({ onloadArgs }); + const { observers, listeners, cleanups } = initObservers({ + onloadArgs, + settings, + }); const { pageActionListener, hashChangeListener, @@ -119,7 +119,7 @@ export default runExtension(async (onloadArgs) => { document.addEventListener("input", discourseNodeSearchTriggerListener); document.addEventListener("selectionchange", nodeCreationPopoverListener); - if (getFeatureFlag("Suggestive mode enabled")) { + if (settings.featureFlags["Suggestive mode enabled"]) { initializeSupabaseSync(); } @@ -150,7 +150,7 @@ export default runExtension(async (onloadArgs) => { getDiscourseNodes: getDiscourseNodes, }; - installDiscourseFloatingMenu(onloadArgs); + installDiscourseFloatingMenu(onloadArgs, settings); const leftSidebarScript = document.querySelector( 'script#roam-left-sidebar[src="https://sid597.github.io/roam-left-sidebar/js/main.js"]', @@ -176,7 +176,7 @@ export default runExtension(async (onloadArgs) => { if (!wrapper) return; if (enabled) { wrapper.style.padding = "0"; - void mountLeftSidebar(wrapper, onloadArgs); + void mountLeftSidebar({ wrapper, onloadArgs }); } else { const root = wrapper.querySelector("#dg-left-sidebar-root"); if (root) { @@ -192,6 +192,10 @@ export default runExtension(async (onloadArgs) => { const { blockUids } = await initSchema(); const cleanupPullWatchers = setupPullWatchOnSettingsPage(blockUids); + console.log( + `[DG Plugin] Total load: ${Math.round(performance.now() - pluginLoadStart)}ms`, + ); + return { elements: [ style, diff --git a/apps/roam/src/utils/findDiscourseNode.ts b/apps/roam/src/utils/findDiscourseNode.ts index 345d0b41d..e0af95981 100644 --- a/apps/roam/src/utils/findDiscourseNode.ts +++ b/apps/roam/src/utils/findDiscourseNode.ts @@ -1,23 +1,27 @@ import getDiscourseNodes, { type DiscourseNode } from "./getDiscourseNodes"; import matchDiscourseNode from "./matchDiscourseNode"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; const discourseNodeTypeCache: Record = {}; const findDiscourseNode = ({ uid, title, - nodes = getDiscourseNodes(), + nodes, + snapshot, }: { uid: string; title?: string; nodes?: DiscourseNode[]; + snapshot?: SettingsSnapshot; }): DiscourseNode | false => { if (typeof discourseNodeTypeCache[uid] !== "undefined") { return discourseNodeTypeCache[uid]; } + const resolvedNodes = nodes ?? getDiscourseNodes(undefined, snapshot); const matchingNode = - nodes.find((node) => + resolvedNodes.find((node) => title === undefined ? matchDiscourseNode({ ...node, uid }) : matchDiscourseNode({ ...node, title }), diff --git a/apps/roam/src/utils/getDiscourseNodes.ts b/apps/roam/src/utils/getDiscourseNodes.ts index cd83f940f..765d12422 100644 --- a/apps/roam/src/utils/getDiscourseNodes.ts +++ b/apps/roam/src/utils/getDiscourseNodes.ts @@ -3,6 +3,7 @@ import getSubTree from "roamjs-components/util/getSubTree"; import { isNewSettingsStoreEnabled, getAllDiscourseNodes, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import discourseConfigRef from "./discourseConfigRef"; import getDiscourseRelations from "./getDiscourseRelations"; @@ -106,9 +107,16 @@ const getUidAndBooleanSetting = ({ }; }; -const getDiscourseNodes = (relations = getDiscourseRelations()) => { +const getDiscourseNodes = ( + relations?: ReturnType, + snapshot?: SettingsSnapshot, +) => { + const resolvedRelations = relations ?? getDiscourseRelations(snapshot); + const newStoreEnabled = snapshot + ? snapshot.featureFlags["Use new settings store"] + : isNewSettingsStoreEnabled(); const configuredNodes = ( - isNewSettingsStoreEnabled() + newStoreEnabled ? getAllDiscourseNodes() : Object.entries(discourseConfigRef.nodes).map( ([type, { text, children }]): DiscourseNode => { @@ -158,7 +166,7 @@ const getDiscourseNodes = (relations = getDiscourseRelations()) => { }, ) ).concat( - relations + resolvedRelations .filter((r) => r.triples.some((t) => t.some((n) => /anchor/i.test(n)))) .map((r) => ({ format: "", diff --git a/apps/roam/src/utils/getDiscourseRelationLabels.ts b/apps/roam/src/utils/getDiscourseRelationLabels.ts index 3355b8f22..33089618c 100644 --- a/apps/roam/src/utils/getDiscourseRelationLabels.ts +++ b/apps/roam/src/utils/getDiscourseRelationLabels.ts @@ -1,8 +1,17 @@ import getDiscourseRelations from "./getDiscourseRelations"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; -const getDiscourseRelationLabels = (relations = getDiscourseRelations()) => - Array.from(new Set(relations.flatMap((r) => [r.label, r.complement]))).filter( - (s) => !!s, - ); +const getDiscourseRelationLabels = ( + relations?: ReturnType, + snapshot?: SettingsSnapshot, +) => + Array.from( + new Set( + (relations ?? getDiscourseRelations(snapshot)).flatMap((r) => [ + r.label, + r.complement, + ]), + ), + ).filter((s) => !!s); export default getDiscourseRelationLabels; diff --git a/apps/roam/src/utils/getDiscourseRelations.ts b/apps/roam/src/utils/getDiscourseRelations.ts index c9f24a911..d7e36cab7 100644 --- a/apps/roam/src/utils/getDiscourseRelations.ts +++ b/apps/roam/src/utils/getDiscourseRelations.ts @@ -9,6 +9,7 @@ import DEFAULT_RELATION_VALUES from "~/data/defaultDiscourseRelations"; import { isNewSettingsStoreEnabled, getAllRelations, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import discourseConfigRef from "./discourseConfigRef"; @@ -35,9 +36,12 @@ export const getRelationsNode = (grammarNode = getGrammarNode()) => { return grammarNode?.children.find(matchNodeText("relations")); }; -const getDiscourseRelations = () => { - if (isNewSettingsStoreEnabled()) { - return getAllRelations(); +const getDiscourseRelations = (snapshot?: SettingsSnapshot) => { + const newStoreEnabled = snapshot + ? snapshot.featureFlags["Use new settings store"] + : isNewSettingsStoreEnabled(); + if (newStoreEnabled) { + return getAllRelations(snapshot); } const grammarNode = getGrammarNode(); diff --git a/apps/roam/src/utils/initializeDiscourseNodes.ts b/apps/roam/src/utils/initializeDiscourseNodes.ts index 46a832b48..847e8fdf7 100644 --- a/apps/roam/src/utils/initializeDiscourseNodes.ts +++ b/apps/roam/src/utils/initializeDiscourseNodes.ts @@ -2,9 +2,12 @@ import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTit import { createPage } from "roamjs-components/writes"; import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes"; import getDiscourseNodes, { excludeDefaultNodes } from "./getDiscourseNodes"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; -const initializeDiscourseNodes = async () => { - const nodes = getDiscourseNodes().filter(excludeDefaultNodes); +const initializeDiscourseNodes = async (snapshot: SettingsSnapshot) => { + const nodes = getDiscourseNodes(undefined, snapshot).filter( + excludeDefaultNodes, + ); if (nodes.length === 0) { await Promise.all( INITIAL_NODE_VALUES.map( diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index c946134ce..27a095aba 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -25,7 +25,9 @@ import { onPageRefObserverChange, getSuggestiveOverlayHandler, } from "~/utils/pageRefObserverHandlers"; -import getDiscourseNodes from "~/utils/getDiscourseNodes"; +import getDiscourseNodes, { + type DiscourseNode, +} from "~/utils/getDiscourseNodes"; import { OnloadArgs } from "roamjs-components/types"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { render as renderGraphOverviewExport } from "~/components/ExportDiscourseContext"; @@ -52,9 +54,8 @@ import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTit import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import findDiscourseNode from "./findDiscourseNode"; import { - getPersonalSetting, - getFeatureFlag, - getGlobalSetting, + bulkReadSettings, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { onSettingChange, @@ -88,8 +89,10 @@ const getTitleAndUidFromHeader = (h1: HTMLHeadingElement) => { export const initObservers = ({ onloadArgs, + settings, }: { onloadArgs: OnloadArgs; + settings: SettingsSnapshot; }): { observers: MutationObserver[]; listeners: { @@ -107,11 +110,18 @@ export const initObservers = ({ callback: (e) => { const h1 = e as HTMLHeadingElement; const { title, uid } = getTitleAndUidFromHeader(h1); - const props = { title, h1, onloadArgs }; - const isSuggestiveModeEnabled = getFeatureFlag("Suggestive mode enabled"); + const settings = bulkReadSettings(); + + const props = { title, h1, onloadArgs }; - const node = findDiscourseNode({ uid, title }); + const isSuggestiveModeEnabled = + settings.featureFlags["Suggestive mode enabled"]; + const node = findDiscourseNode({ + uid, + title, + snapshot: settings, + }); const isDiscourseNode = node && node.backedBy !== "default"; if (isDiscourseNode) { renderDiscourseContext({ h1, uid }); @@ -125,10 +135,13 @@ export const initObservers = ({ renderCanvasReferences(linkedReferencesDiv, uid, onloadArgs); } } - - if (isQueryPage({ title })) renderQueryPage(props); - else if (isCurrentPageCanvas(props)) renderTldrawCanvas(props); - else if (isSidebarCanvas(props)) renderTldrawCanvasInSidebar(props); + if (isQueryPage({ title, snapshot: settings })) { + renderQueryPage(props); + } else if (isCurrentPageCanvas({ title, h1, snapshot: settings })) { + renderTldrawCanvas(props); + } else if (isSidebarCanvas({ title, h1, snapshot: settings })) { + renderTldrawCanvasInSidebar(props); + } }, }); @@ -137,6 +150,18 @@ export const initObservers = ({ render: (b) => renderQueryBlock(b, onloadArgs), }); + let batchedTagNodes: DiscourseNode[] | null = null; + const getNodesForTagBatch = (): DiscourseNode[] => { + if (batchedTagNodes === null) { + const settings = bulkReadSettings(); + batchedTagNodes = getDiscourseNodes(undefined, settings); + queueMicrotask(() => { + batchedTagNodes = null; + }); + } + return batchedTagNodes; + }; + const nodeTagPopupButtonObserver = createHTMLObserver({ className: "rm-page-ref--tag", tag: "SPAN", @@ -145,7 +170,7 @@ export const initObservers = ({ if (tag) { const normalizedTag = getCleanTagText(tag); - for (const node of getDiscourseNodes()) { + for (const node of getNodesForTagBatch()) { const normalizedNodeTag = node.tag ? getCleanTagText(node.tag) : ""; if (normalizedTag === normalizedNodeTag) { renderNodeTagPopupButton(s, node, onloadArgs.extensionAPI); @@ -203,7 +228,7 @@ export const initObservers = ({ const suggestiveHandler = getSuggestiveOverlayHandler(onloadArgs); const toggleSuggestiveOverlay = onPageRefObserverChange(suggestiveHandler); - if (getPersonalSetting([PERSONAL_KEYS.suggestiveModeOverlay])) { + if (settings.personalSettings[PERSONAL_KEYS.suggestiveModeOverlay]) { addPageRefObserver(suggestiveHandler); } @@ -233,34 +258,40 @@ export const initObservers = ({ }, }); - if (getPersonalSetting([PERSONAL_KEYS.pagePreview])) + if (settings.personalSettings[PERSONAL_KEYS.pagePreview]) addPageRefObserver(previewPageRefHandler); - if (getPersonalSetting([PERSONAL_KEYS.discourseContextOverlay])) { + + if (settings.personalSettings[PERSONAL_KEYS.discourseContextOverlay]) { const overlayHandler = getOverlayHandler(onloadArgs); onPageRefObserverChange(overlayHandler)(true); } + if (getPageRefObserversSize()) enablePageRefObserver(); const configPageUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); const hashChangeListener = (e: Event) => { const evt = e as HashChangeEvent; + const settings = bulkReadSettings(); // Attempt to refresh config navigating away from config page // doesn't work if they update via sidebar if ( (configPageUid && evt.oldURL.endsWith(configPageUid)) || - getDiscourseNodes().some(({ type }) => evt.oldURL.endsWith(type)) + getDiscourseNodes(undefined, settings).some(({ type }) => + evt.oldURL.endsWith(type), + ) ) { - refreshConfigTree(); + refreshConfigTree(settings); } }; - let globalTrigger = ( - getGlobalSetting([GLOBAL_KEYS.trigger]) ?? "\\" - ).trim(); - const personalTriggerCombo = getPersonalSetting([ - PERSONAL_KEYS.personalNodeMenuTrigger, - ]); + let globalTrigger = settings.globalSettings[GLOBAL_KEYS.trigger].trim(); + const personalTriggerComboRaw = + settings.personalSettings[PERSONAL_KEYS.personalNodeMenuTrigger]; + const personalTriggerCombo = + typeof personalTriggerComboRaw === "object" + ? personalTriggerComboRaw + : undefined; let personalTrigger = personalTriggerCombo?.key; let personalModifiers = personalTriggerCombo ? getModifiersFromCombo(personalTriggerCombo) @@ -291,11 +322,17 @@ export const initObservers = ({ className: "starred-pages-wrapper", callback: (el) => { void (async () => { - const isLeftSidebarEnabled = getFeatureFlag("Enable left sidebar"); + const settings = bulkReadSettings(); + const isLeftSidebarEnabled = + settings.featureFlags["Enable left sidebar"]; const container = el as HTMLDivElement; if (isLeftSidebarEnabled) { container.style.padding = "0"; - await mountLeftSidebar(container, onloadArgs); + await mountLeftSidebar({ + wrapper: container, + onloadArgs, + initialSnapshot: settings, + }); } })(); }, @@ -343,7 +380,7 @@ export const initObservers = ({ }; let customTrigger = - getPersonalSetting([PERSONAL_KEYS.nodeSearchMenuTrigger]) ?? "@"; + settings.personalSettings[PERSONAL_KEYS.nodeSearchMenuTrigger]; const unsubSearchTrigger = onSettingChange( settingKeys.nodeSearchMenuTrigger, @@ -403,10 +440,8 @@ export const initObservers = ({ }; const nodeCreationPopoverListener = debounce(() => { - const isTextSelectionPopupEnabled = - getPersonalSetting([PERSONAL_KEYS.textSelectionPopup]) !== false; - - if (!isTextSelectionPopupEnabled) return; + const settings = bulkReadSettings(); + if (!settings.personalSettings[PERSONAL_KEYS.textSelectionPopup]) return; const selection = window.getSelection(); diff --git a/apps/roam/src/utils/isCanvasPage.ts b/apps/roam/src/utils/isCanvasPage.ts index a9a3b7a49..4c7d97331 100644 --- a/apps/roam/src/utils/isCanvasPage.ts +++ b/apps/roam/src/utils/isCanvasPage.ts @@ -1,10 +1,21 @@ import { DEFAULT_CANVAS_PAGE_FORMAT } from ".."; -import { getGlobalSetting } from "~/components/settings/utils/accessors"; +import { + getGlobalSetting, + type SettingsSnapshot, +} from "~/components/settings/utils/accessors"; import { GLOBAL_KEYS } from "~/components/settings/utils/settingKeys"; -export const isCanvasPage = ({ title }: { title: string }) => { +export const isCanvasPage = ({ + title, + snapshot, +}: { + title: string; + snapshot?: SettingsSnapshot; +}) => { const format = - getGlobalSetting([GLOBAL_KEYS.canvasPageFormat]) || + (snapshot + ? snapshot.globalSettings[GLOBAL_KEYS.canvasPageFormat] + : getGlobalSetting([GLOBAL_KEYS.canvasPageFormat])) || DEFAULT_CANVAS_PAGE_FORMAT; const canvasRegex = new RegExp(`^${format}$`.replace(/\*/g, ".+")); return canvasRegex.test(title); @@ -13,19 +24,25 @@ export const isCanvasPage = ({ title }: { title: string }) => { export const isCurrentPageCanvas = ({ title, h1, + snapshot, }: { title: string; h1: HTMLHeadingElement; + snapshot: SettingsSnapshot; }) => { - return isCanvasPage({ title }) && !!h1.closest(".roam-article"); + return isCanvasPage({ title, snapshot }) && !!h1.closest(".roam-article"); }; export const isSidebarCanvas = ({ title, h1, + snapshot, }: { title: string; h1: HTMLHeadingElement; + snapshot: SettingsSnapshot; }) => { - return isCanvasPage({ title }) && !!h1.closest(".rm-sidebar-outline"); + return ( + isCanvasPage({ title, snapshot }) && !!h1.closest(".rm-sidebar-outline") + ); }; diff --git a/apps/roam/src/utils/isQueryPage.ts b/apps/roam/src/utils/isQueryPage.ts index aa3df2255..4e6109944 100644 --- a/apps/roam/src/utils/isQueryPage.ts +++ b/apps/roam/src/utils/isQueryPage.ts @@ -1,7 +1,14 @@ import { getQueryPages } from "~/components/settings/QueryPagesPanel"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; -export const isQueryPage = ({ title }: { title: string }): boolean => { - const queryPages = getQueryPages(); +export const isQueryPage = ({ + title, + snapshot, +}: { + title: string; + snapshot: SettingsSnapshot; +}): boolean => { + const queryPages = getQueryPages(snapshot); const matchesQueryPage = queryPages.some((queryPage) => { const escapedPattern = queryPage diff --git a/apps/roam/src/utils/posthog.ts b/apps/roam/src/utils/posthog.ts index 407c8676d..c2b4bc8a5 100644 --- a/apps/roam/src/utils/posthog.ts +++ b/apps/roam/src/utils/posthog.ts @@ -2,13 +2,13 @@ import getCurrentUserUid from "roamjs-components/queries/getCurrentUserUid"; import { getVersionWithDate } from "./getVersion"; import posthog from "posthog-js"; import type { CaptureResult } from "posthog-js"; -import { getPersonalSetting } from "~/components/settings/utils/accessors"; -import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys"; let initialized = false; -const doInitPostHog = (): void => { +export const initPostHog = (): void => { if (initialized) return; + if (window.roamAlphaAPI.graph.isEncrypted) return; + if (window.roamAlphaAPI.graph.type === "offline") return; const propertyDenylist = new Set([ "$ip", "$device_id", @@ -58,19 +58,10 @@ const doInitPostHog = (): void => { }; export const enablePostHog = (): void => { - doInitPostHog(); + initPostHog(); posthog.opt_in_capturing(); }; export const disablePostHog = (): void => { if (initialized) posthog.opt_out_capturing(); }; - -export const initPostHog = (): void => { - const disabled = getPersonalSetting([ - PERSONAL_KEYS.disableProductDiagnostics, - ]); - if (!disabled) { - doInitPostHog(); - } -}; diff --git a/apps/roam/src/utils/refreshConfigTree.ts b/apps/roam/src/utils/refreshConfigTree.ts index 606d8097c..8d23401c8 100644 --- a/apps/roam/src/utils/refreshConfigTree.ts +++ b/apps/roam/src/utils/refreshConfigTree.ts @@ -6,6 +6,7 @@ import registerDiscourseDatalogTranslators from "./registerDiscourseDatalogTrans import { unregisterDatalogTranslator } from "./conditionToDatalog"; import type { PullBlock } from "roamjs-components/types/native"; import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/data/constants"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; const getPagesStartingWithPrefix = (prefix: string) => ( @@ -17,8 +18,8 @@ const getPagesStartingWithPrefix = (prefix: string) => uid: r[0][":block/uid"] || "", })); -const refreshConfigTree = () => { - getDiscourseRelationLabels().forEach((key) => +const refreshConfigTree = (snapshot?: SettingsSnapshot) => { + getDiscourseRelationLabels(undefined, snapshot).forEach((key) => unregisterDatalogTranslator({ key }), ); discourseConfigRef.tree = getBasicTreeByParentUid( @@ -36,7 +37,7 @@ const refreshConfigTree = () => { ]; }), ); - registerDiscourseDatalogTranslators(); + registerDiscourseDatalogTranslators(snapshot); }; export default refreshConfigTree; diff --git a/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts b/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts index 091ec978d..b9bd53719 100644 --- a/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts +++ b/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts @@ -21,6 +21,7 @@ import { fireQuerySync, getWhereClauses } from "./fireQuery"; import { toVar } from "./compileDatalog"; import { getExistingRelationPageUid } from "./createReifiedBlock"; import { getStoredRelationsEnabled } from "./storedRelations"; +import type { SettingsSnapshot } from "~/components/settings/utils/accessors"; const hasTag = (node: DiscourseNode): node is DiscourseNode & { tag: string } => !!node.tag; @@ -87,9 +88,9 @@ const collectVariables = (clauses: DatalogClause[]): Set => const ANY_DISCOURSE_NODE = "Any discourse node"; -const registerDiscourseDatalogTranslators = () => { - const discourseRelations = getDiscourseRelations(); - const discourseNodes = getDiscourseNodes(discourseRelations); +const registerDiscourseDatalogTranslators = (snapshot?: SettingsSnapshot) => { + const discourseRelations = getDiscourseRelations(snapshot); + const discourseNodes = getDiscourseNodes(discourseRelations, snapshot); const isACallback: Parameters< typeof registerDatalogTranslator diff --git a/apps/roam/src/utils/setQueryPages.ts b/apps/roam/src/utils/setQueryPages.ts index cf5a8be13..f75fb521b 100644 --- a/apps/roam/src/utils/setQueryPages.ts +++ b/apps/roam/src/utils/setQueryPages.ts @@ -1,25 +1,19 @@ import { OnloadArgs } from "roamjs-components/types"; import { - getPersonalSetting, setPersonalSetting, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS, QUERY_KEYS, } from "~/components/settings/utils/settingKeys"; -export const setInitialQueryPages = (onloadArgs: OnloadArgs) => { - // Legacy extensionAPI stored query-pages as string | string[] | Record. - // Coerce to string[] for backward compatibility with old stored formats. - const raw = getPersonalSetting>([ - PERSONAL_KEYS.query, - QUERY_KEYS.queryPages, - ]); - const queryPageArray = Array.isArray(raw) - ? raw - : typeof raw === "string" && raw - ? [raw] - : []; +export const setInitialQueryPages = ( + onloadArgs: OnloadArgs, + snapshot: SettingsSnapshot, +) => { + const queryPageArray = + snapshot.personalSettings[PERSONAL_KEYS.query][QUERY_KEYS.queryPages]; if (!queryPageArray.includes("discourse-graph/queries/*")) { const updated = [...queryPageArray, "discourse-graph/queries/*"]; void onloadArgs.extensionAPI.settings.set("query-pages", updated);