diff --git a/package-lock.json b/package-lock.json index 520f8df..7a5f800 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6724,6 +6724,15 @@ "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==", "dev": true }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lodash.merge": { "version": "4.6.9", "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", @@ -18562,6 +18571,7 @@ "deep-equal": "^2.2.3", "esbuild-plugin-flow": "github:dalcib/esbuild-plugin-flow", "immer": "^10.1.1", + "lodash.debounce": "^4.0.8", "lodash.merge": "^4.6.2", "react": "^18.3.1", "react-art": "^18.3.1", @@ -18582,6 +18592,7 @@ "@types/chrome": "^0.0.270", "@types/deep-equal": "^1.0.4", "@types/jest": "^29.5.13", + "@types/lodash.debounce": "^4.0.9", "@types/lodash.merge": "^4.6.9", "@types/node": "^22.5.4", "@types/react": "^18.3.5", @@ -18875,6 +18886,7 @@ "classnames": "^2.3.2", "cytoscape": "^3.30.4", "cytoscape-elk": "^2.2.0", + "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "react": "^18.2.0", "react-cytoscapejs": "^2.0.0", @@ -18891,6 +18903,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@types/cytoscape": "^3.19.11", + "@types/lodash.debounce": "^4.0.9", "@types/lodash.throttle": "^4.1.9", "@types/react": "^18.2.15", "babel-jest": "^29.7.0", diff --git a/packages/browser-extension/package.json b/packages/browser-extension/package.json index 02e8e94..a21847b 100644 --- a/packages/browser-extension/package.json +++ b/packages/browser-extension/package.json @@ -31,14 +31,15 @@ "dependencies": { "@reduxjs/toolkit": "^2.2.7", "@sentry/browser": "^8.33.1", + "@sophistree/common": "file:../common", + "@sophistree/ui-common": "file:../ui-common", "classnames": "^2.5.1", "cytoscape": "^3.30.2", "cytoscape-elk": "^2.2.0", "deep-equal": "^2.2.3", "esbuild-plugin-flow": "github:dalcib/esbuild-plugin-flow", - "@sophistree/common": "file:../common", - "@sophistree/ui-common": "file:../ui-common", "immer": "^10.1.1", + "lodash.debounce": "^4.0.8", "lodash.merge": "^4.6.2", "react": "^18.3.1", "react-art": "^18.3.1", @@ -59,6 +60,7 @@ "@types/chrome": "^0.0.270", "@types/deep-equal": "^1.0.4", "@types/jest": "^29.5.13", + "@types/lodash.debounce": "^4.0.9", "@types/lodash.merge": "^4.6.9", "@types/node": "^22.5.4", "@types/react": "^18.3.5", diff --git a/packages/browser-extension/src/components/EntityEditor.tsx b/packages/browser-extension/src/components/EntityEditor.tsx index a0f7fcc..4fef687 100644 --- a/packages/browser-extension/src/components/EntityEditor.tsx +++ b/packages/browser-extension/src/components/EntityEditor.tsx @@ -1,7 +1,8 @@ -import React from "react"; +import React, { useMemo, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { TextInput, Text, Surface } from "react-native-paper"; import { View, StyleSheet } from "react-native"; +import debounce from "lodash.debounce"; import { Polarity, @@ -47,15 +48,28 @@ function chooseEditor(entity: Entity) { function PropositionEditor({ entity }: { entity: Proposition }) { const dispatch = useDispatch(); + const [text, setText] = useState(entity.text); + + const debouncedDispatch = useMemo( + () => + debounce((text: string) => { + dispatch(updateProposition({ id: entity.id, updates: { text } })); + }, 150), + [dispatch, entity.id], + ); + + const handleTextChange = (newText: string) => { + setText(newText); + debouncedDispatch(newText); + }; + return ( - dispatch(updateProposition({ id: entity.id, updates: { text } })) - } + onChangeText={handleTextChange} /> ); @@ -84,22 +98,35 @@ function JustificationEditor({ entity }: { entity: Justification }) { function MediaExcerptEditor({ entity }: { entity: MediaExcerpt }) { const dispatch = useDispatch(); + const [text, setText] = useState(entity.sourceInfo.name); + + const debouncedDispatch = useMemo( + () => + debounce((text: string) => { + dispatch( + updateMediaExerpt({ + id: entity.id, + updates: { sourceInfo: { name: text } }, + }), + ); + }, 150), + [dispatch, entity.id], + ); + + const handleTextChange = (newText: string) => { + setText(newText); + debouncedDispatch(newText); + }; + return ( - dispatch( - updateMediaExerpt({ - id: entity.id, - updates: { sourceInfo: { name: text } }, - }), - ) - } + onChangeText={handleTextChange} /> Quotation diff --git a/packages/ui-common/package.json b/packages/ui-common/package.json index f7e796e..7643ed7 100644 --- a/packages/ui-common/package.json +++ b/packages/ui-common/package.json @@ -20,6 +20,7 @@ "classnames": "^2.3.2", "cytoscape": "^3.30.4", "cytoscape-elk": "^2.2.0", + "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "react": "^18.2.0", "react-cytoscapejs": "^2.0.0", @@ -36,6 +37,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@types/cytoscape": "^3.19.11", + "@types/lodash.debounce": "^4.0.9", "@types/lodash.throttle": "^4.1.9", "@types/react": "^18.2.15", "babel-jest": "^29.7.0", diff --git a/packages/ui-common/src/cytoscape/reactNodes.tsx b/packages/ui-common/src/cytoscape/reactNodes.tsx index 607d749..8279466 100644 --- a/packages/ui-common/src/cytoscape/reactNodes.tsx +++ b/packages/ui-common/src/cytoscape/reactNodes.tsx @@ -5,6 +5,7 @@ import cytoscape, { } from "cytoscape"; import ReactDOM from "react-dom/client"; import throttle from "lodash.throttle"; +import debounce from "lodash.debounce"; import { sunflower } from "../colors"; @@ -23,6 +24,8 @@ interface ReactNodesOptions { layoutOptions: LayoutOptions; // The delay before reactNodes applies a layout when one is necessary. layoutThrottleDelay?: number; + // The delay before rendering the JSX after the node data changes. + nodeDataRenderDelay?: number; logger: Logger; } @@ -37,10 +40,14 @@ export interface ReactNodeOptions { unselectedStyle?: Partial; } -const defaultOptions: Required> = - { - layoutThrottleDelay: 100, - }; +type PassthroughOptions = Pick; + +const defaultOptions: Required< + Pick +> = { + layoutThrottleDelay: 500, + nodeDataRenderDelay: 150, +}; const defaultReactNodeOptions: ReactNodeOptions = { query: "node", // selector for nodes to apply HTML to @@ -65,8 +72,14 @@ function reactNodes(this: cytoscape.Core, options: ReactNodesOptions) { this.layout(options.layoutOptions).run(); }, options.layoutThrottleDelay ?? defaultOptions.layoutThrottleDelay); + const { nodeDataRenderDelay } = options; options.nodes.forEach((nodeOptions) => - makeReactNode(this, options.logger, nodeOptions, layout), + makeReactNode( + this, + options.logger, + { ...nodeOptions, nodeDataRenderDelay }, + layout, + ), ); applyWebkitLayoutWorkaround(this, options.layoutOptions); @@ -111,7 +124,7 @@ function applyWebkitLayoutWorkaround( function makeReactNode( cy: cytoscape.Core, logger: Logger, - options: ReactNodeOptions, + options: ReactNodeOptions & PassthroughOptions, layout: () => void, ) { options = Object.assign({}, defaultReactNodeOptions, options); @@ -170,9 +183,14 @@ function makeReactNode( Object.assign(htmlElement.style, options.unselectedStyle); } }); - node.on("data", function renderReactNode() { + + // Debounce the node data event handler to reduce Redux updates + const debouncedDataHandler = debounce(function renderReactNode() { renderJsxElement(reactRoot); - }); + }, options.nodeDataRenderDelay); + + node.on("data", debouncedDataHandler); + if (options.syncClasses) { node.on("style", syncNodeClasses); }