From b61a2225b74f13f4b8eab4b292a43b8de5677239 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Dec 2024 12:28:27 +0000 Subject: [PATCH 01/13] Switch to React18 useId Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/settings/AvatarSetting.tsx | 3 +-- .../views/settings/UserProfileSettings.tsx | 3 +-- src/utils/useId.ts | 16 ---------------- 3 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 src/utils/useId.ts diff --git a/src/components/views/settings/AvatarSetting.tsx b/src/components/views/settings/AvatarSetting.tsx index b6ce5415903..ee47094cf93 100644 --- a/src/components/views/settings/AvatarSetting.tsx +++ b/src/components/views/settings/AvatarSetting.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ReactNode, createRef, useCallback, useEffect, useState } from "react"; +import React, { ReactNode, createRef, useCallback, useEffect, useState, useId } from "react"; import EditIcon from "@vector-im/compound-design-tokens/assets/web/icons/edit"; import UploadIcon from "@vector-im/compound-design-tokens/assets/web/icons/share"; import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete"; @@ -16,7 +16,6 @@ import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { mediaFromMxc } from "../../../customisations/Media"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; -import { useId } from "../../../utils/useId"; import AccessibleButton from "../elements/AccessibleButton"; import BaseAvatar from "../avatars/BaseAvatar"; import Modal from "../../../Modal.tsx"; diff --git a/src/components/views/settings/UserProfileSettings.tsx b/src/components/views/settings/UserProfileSettings.tsx index 403b6349c9d..83a00c122d4 100644 --- a/src/components/views/settings/UserProfileSettings.tsx +++ b/src/components/views/settings/UserProfileSettings.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ChangeEvent, ReactNode, useCallback, useEffect, useMemo, useState } from "react"; +import React, { ChangeEvent, ReactNode, useCallback, useEffect, useMemo, useState, useId } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { EditInPlace, Alert, ErrorMessage } from "@vector-im/compound-web"; import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; @@ -20,7 +20,6 @@ import { formatBytes } from "../../../utils/FormattingUtils"; import { useToastContext } from "../../../contexts/ToastContext"; import InlineSpinner from "../elements/InlineSpinner"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; -import { useId } from "../../../utils/useId"; import CopyableText from "../elements/CopyableText"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import AccessibleButton from "../elements/AccessibleButton"; diff --git a/src/utils/useId.ts b/src/utils/useId.ts deleted file mode 100644 index 6f7cf795980..00000000000 --- a/src/utils/useId.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2024 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; - -const getUniqueId = (() => { - return () => `:r${Math.random()}:`; -})(); - -// Replace this with React's own useId once we switch to React 18 -export const useId = (): string => React.useMemo(getUniqueId, []); From c8c5ef5e6ecd0ff2618cdb2b43121736edd344ac Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Dec 2024 12:31:18 +0000 Subject: [PATCH 02/13] Enable react-compiler eslint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintrc.js | 4 +++- package.json | 1 + yarn.lock | 41 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 2b0dd2c186b..e302a526737 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - plugins: ["matrix-org"], + plugins: ["matrix-org", "eslint-plugin-react-compiler"], extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"], parserOptions: { project: ["./tsconfig.json"], @@ -170,6 +170,8 @@ module.exports = { "jsx-a11y/role-supports-aria-props": "off", "matrix-org/require-copyright-header": "error", + + "react-compiler/react-compiler": "error", }, overrides: [ { diff --git a/package.json b/package.json index 5a75055510b..902852a8ef4 100644 --- a/package.json +++ b/package.json @@ -233,6 +233,7 @@ "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-matrix-org": "^2.0.2", "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-unicorn": "^56.0.0", "express": "^4.18.2", diff --git a/yarn.lock b/yarn.lock index 551c5b4d6e7..6ed22a8c2ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -56,7 +56,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== -"@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.23.9": +"@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.23.9", "@babel/core@^7.24.4": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== @@ -303,6 +303,13 @@ dependencies: "@babel/types" "^7.25.8" +"@babel/parser@^7.24.4": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== + dependencies: + "@babel/types" "^7.26.3" + "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" @@ -1150,6 +1157,14 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" +"@babel/types@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -5628,6 +5643,18 @@ eslint-plugin-matrix-org@^2.0.2: resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-2.0.2.tgz#95b86b0f16704ab19740f7c3c62eae69e20365e6" integrity sha512-cQy5Rjeq6uyu1mLXlPZwEJdyM0NmclrnEz68y792FSuuxzMyJNNYLGDQ5CkYW8H+PrD825HUFZ34pNXnjMOzOw== +eslint-plugin-react-compiler@^19.0.0-beta-df7b47d-20241124: + version "19.0.0-beta-df7b47d-20241124" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-df7b47d-20241124.tgz#468751d3a8a6781189405ee56b39b80545306df8" + integrity sha512-82PfnllC8jP/68KdLAbpWuYTcfmtGLzkqy2IW85WopKMTr+4rdQpp+lfliQ/QE79wWrv/dRoADrk3Pdhq25nTw== + dependencies: + "@babel/core" "^7.24.4" + "@babel/parser" "^7.24.4" + "@babel/plugin-transform-private-methods" "^7.25.9" + hermes-parser "^0.25.1" + zod "^3.22.4" + zod-validation-error "^3.0.3" + eslint-plugin-react-hooks@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101" @@ -6574,6 +6601,18 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hermes-estree@0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480" + integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw== + +hermes-parser@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1" + integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA== + dependencies: + hermes-estree "0.25.1" + highlight.js@^11.3.1: version "11.10.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92" From e730074e1b3775821fa6a1ca5951111cb26dc9d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Dec 2024 12:31:33 +0000 Subject: [PATCH 03/13] Fix an easy one Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/EffectsOverlay.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index 3e5a5ead60a..ad7d9c825e3 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -58,11 +58,10 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { if (canvas) canvas.height = UIStore.instance.windowHeight; UIStore.instance.on(UI_EVENTS.Resize, resize); + const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored return () => { dis.unregister(dispatcherRef); UIStore.instance.off(UI_EVENTS.Resize, resize); - // eslint-disable-next-line react-hooks/exhaustive-deps - const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored for (const effect in currentEffects) { const effectModule: ICanvasEffect = currentEffects.get(effect)!; if (effectModule && effectModule.isRunning) { From eb1a09a91285b77e081a1106caf4b668e4482cd2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Dec 2024 12:32:36 +0000 Subject: [PATCH 04/13] Disable in tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index e302a526737..a017112b4e7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -264,6 +264,7 @@ module.exports = { // These are fine in tests "no-restricted-globals": "off", + "react-compiler/react-compiler": "off", }, }, { From 9443426edba2d3313175a89d9b9222d87538886e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Dec 2024 12:57:39 +0000 Subject: [PATCH 05/13] Fix usage of useRef as memoization Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomSearchView.tsx | 4 ++-- .../rooms/wysiwyg_composer/EditWysiwygComposer.tsx | 6 +++--- .../rooms/wysiwyg_composer/SendWysiwygComposer.tsx | 6 +++--- src/components/views/settings/ThemeChoicePanel.tsx | 6 +++--- src/contexts/ToastContext.tsx | 12 ++++++------ 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index 82146bcc5e0..9e6263bcfb7 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react"; +import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { ISearchResults, IThreadBundledRelationship, @@ -58,7 +58,7 @@ export const RoomSearchView = forwardRef( const [results, setResults] = useState(null); const aborted = useRef(false); // A map from room ID to permalink creator - const permalinkCreators = useRef(new Map()).current; + const permalinkCreators = useMemo(() => new Map(), []); const innerRef = useRef(); useEffect(() => { diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index 98597c73609..a28d2746463 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react"; +import React, { ForwardedRef, forwardRef, MutableRefObject, useMemo } from "react"; import classNames from "classnames"; import EditorStateTransfer from "../../../../utils/EditorStateTransfer"; @@ -44,7 +44,7 @@ export default function EditWysiwygComposer({ className, ...props }: EditWysiwygComposerProps): JSX.Element { - const defaultContextValue = useRef(getDefaultContextValue({ editorStateTransfer })); + const defaultContextValue = useMemo(() => getDefaultContextValue({ editorStateTransfer }), []); const initialContent = useInitialContent(editorStateTransfer); const isReady = !editorStateTransfer || initialContent !== undefined; @@ -55,7 +55,7 @@ export default function EditWysiwygComposer({ } return ( - + getDefaultContextValue({ eventRelation: props.eventRelation }), []); return ( - + } diff --git a/src/components/views/settings/ThemeChoicePanel.tsx b/src/components/views/settings/ThemeChoicePanel.tsx index 83f17a2f7be..3355913abd8 100644 --- a/src/components/views/settings/ThemeChoicePanel.tsx +++ b/src/components/views/settings/ThemeChoicePanel.tsx @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { ChangeEvent, JSX, useCallback, useMemo, useRef, useState } from "react"; +import React, { ChangeEvent, JSX, useCallback, useMemo, useState } from "react"; import { InlineField, ToggleControl, @@ -39,12 +39,12 @@ import { useSettingValue } from "../../../hooks/useSettings"; */ export function ThemeChoicePanel(): JSX.Element { const themeState = useTheme(); - const themeWatcher = useRef(new ThemeWatcher()); + const themeWatcher = useMemo(() => new ThemeWatcher(), []); const customThemeEnabled = useSettingValue("feature_custom_themes"); return ( - {themeWatcher.current.isSystemThemeSupported() && ( + {themeWatcher.isSystemThemeSupported() && ( )} diff --git a/src/contexts/ToastContext.tsx b/src/contexts/ToastContext.tsx index 4ae4875c96b..e9bd392a60c 100644 --- a/src/contexts/ToastContext.tsx +++ b/src/contexts/ToastContext.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; +import { ReactNode, createContext, useCallback, useContext, useEffect, useState, useMemo } from "react"; /** * A ToastContext helps components display any kind of toast message and can be provided @@ -33,19 +33,19 @@ export function useToastContext(): ToastRack { * the ToastRack object that should be provided to the context */ export function useActiveToast(): [ReactNode | undefined, ToastRack] { - const toastRack = useRef(new ToastRack()); + const toastRack = useMemo(() => new ToastRack(), []); - const [activeToast, setActiveToast] = useState(toastRack.current.getActiveToast()); + const [activeToast, setActiveToast] = useState(toastRack.getActiveToast()); const updateCallback = useCallback(() => { - setActiveToast(toastRack.current.getActiveToast()); + setActiveToast(toastRack.getActiveToast()); }, [setActiveToast, toastRack]); useEffect(() => { - toastRack.current.setCallback(updateCallback); + toastRack.setCallback(updateCallback); }, [toastRack, updateCallback]); - return [activeToast, toastRack.current]; + return [activeToast, toastRack]; } interface DisplayedToast { From b597abf56729d20796b5940f55236675d4e6ce3b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Dec 2024 12:59:35 +0000 Subject: [PATCH 06/13] Fix mutation of external values in hooks Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/tabs/room/VoipRoomSettingsTab.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 783ea1bce3b..a428b7a1486 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -47,21 +47,24 @@ const ElementCallSwitch: React.FC = ({ room }) => { (enabled: boolean): void => { setElementCallEnabled(enabled); + // Take a copy to avoid mutating the original + const newEvents = { ...events }; + if (enabled) { - const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0; + const userLevel = newEvents[EventType.RoomMessage] ?? content.users_default ?? 0; const moderatorLevel = content.kick ?? 50; - events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel; - events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel; + newEvents[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel; + newEvents[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel; } else { - const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100; + const adminLevel = newEvents[EventType.RoomPowerLevels] ?? content.state_default ?? 100; - events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel; - events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; + newEvents[ElementCall.CALL_EVENT_TYPE.name] = adminLevel; + newEvents[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; } room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, { - events: events, + events: newEvents, ...content, }); }, From 72f155640de204d2412657b508b73cb5ff0abe2e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Dec 2024 12:59:53 +0000 Subject: [PATCH 07/13] Make React compiler happy about some frankly non-issues Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/spaces/QuickThemeSwitcher.tsx | 2 +- .../useUnreadThreadRooms.ts | 16 ++++--- src/utils/location/useMap.ts | 42 +++++++++---------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/components/views/spaces/QuickThemeSwitcher.tsx b/src/components/views/spaces/QuickThemeSwitcher.tsx index 195fcb98991..f4c229ae04c 100644 --- a/src/components/views/spaces/QuickThemeSwitcher.tsx +++ b/src/components/views/spaces/QuickThemeSwitcher.tsx @@ -27,7 +27,7 @@ type Props = { const MATCH_SYSTEM_THEME_ID = "MATCH_SYSTEM_THEME_ID"; const QuickThemeSwitcher: React.FC = ({ requestClose }) => { - const orderedThemes = useMemo(getOrderedThemes, []); + const orderedThemes = useMemo(() => getOrderedThemes(), []); const themeState = useTheme(); const nonHighContrast = findNonHighContrastTheme(themeState.theme); diff --git a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts index 110c9d51f83..1ea10bed682 100644 --- a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts +++ b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { ClientEvent, MatrixClient, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix"; import { throttle } from "lodash"; @@ -42,14 +42,12 @@ export function useUnreadThreadRooms(forceComputation: boolean): Result { setResult(computeUnreadThreadRooms(mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs)); }, [mxClient, msc3946ProcessDynamicPredecessor, settingTACOnlyNotifs]); - // The exhautive deps lint rule can't compute dependencies here since it's not a plain inline func. - // We make this as simple as possible so its only dep is doUpdate itself. - // eslint-disable-next-line react-hooks/exhaustive-deps - const scheduleUpdate = useCallback( - throttle(doUpdate, MIN_UPDATE_INTERVAL_MS, { - leading: false, - trailing: true, - }), + const scheduleUpdate = useMemo( + () => + throttle(doUpdate, MIN_UPDATE_INTERVAL_MS, { + leading: false, + trailing: true, + }), [doUpdate], ); diff --git a/src/utils/location/useMap.ts b/src/utils/location/useMap.ts index 308aedc205c..300a15a4ec1 100644 --- a/src/utils/location/useMap.ts +++ b/src/utils/location/useMap.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { useEffect, useState } from "react"; +import { useEffect, useMemo } from "react"; import type { Map as MapLibreMap } from "maplibre-gl"; import { createMap } from "./map"; @@ -26,29 +26,25 @@ interface UseMapProps { */ export const useMap = ({ interactive, bodyId, onError }: UseMapProps): MapLibreMap | undefined => { const cli = useMatrixClientContext(); - const [map, setMap] = useState(); - - useEffect( - () => { - try { - setMap(createMap(cli, !!interactive, bodyId, onError)); - } catch (error) { - console.error("Error encountered in useMap", error); - if (error instanceof Error) { - onError?.(error); - } + + const map = useMemo(() => { + try { + return createMap(cli, !!interactive, bodyId, onError); + } catch (error) { + console.error("Error encountered in useMap", error); + if (error instanceof Error) { + onError?.(error); } - return () => { - if (map) { - map.remove(); - setMap(undefined); - } - }; - }, - // map is excluded as a dependency - // eslint-disable-next-line react-hooks/exhaustive-deps - [interactive, bodyId, onError], - ); + } + }, [bodyId, cli, interactive, onError]); + + // cleanup + useEffect(() => { + if (!map) return; + return () => { + map.remove(); + }; + }, [map]); return map; }; From c3f3c9364fe1430674b05979aa7cbd74fd6038f2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Dec 2024 13:14:13 +0000 Subject: [PATCH 08/13] Fix MapMock Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- __mocks__/maplibre-gl.js | 1 + 1 file changed, 1 insertion(+) diff --git a/__mocks__/maplibre-gl.js b/__mocks__/maplibre-gl.js index c410e4f24c5..b47d4c02f80 100644 --- a/__mocks__/maplibre-gl.js +++ b/__mocks__/maplibre-gl.js @@ -17,6 +17,7 @@ class MockMap extends EventEmitter { setCenter = jest.fn(); setStyle = jest.fn(); fitBounds = jest.fn(); + remove = jest.fn(); } const MockMapInstance = new MockMap(); From 6946b90b11eef70174f7155dcd05abb94e8cac02 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Dec 2024 13:27:34 +0000 Subject: [PATCH 09/13] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../tabs/room/VoipRoomSettingsTab.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index a428b7a1486..14de26629b0 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { useCallback, useMemo, useState } from "react"; import { JoinRule, EventType, RoomState, Room } from "matrix-js-sdk/src/matrix"; +import { RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types"; import { _t } from "../../../../../languageHandler"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; @@ -24,23 +25,24 @@ interface ElementCallSwitchProps { const ElementCallSwitch: React.FC = ({ room }) => { const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]); - const [content, events, maySend] = useRoomState( + const [content, maySend] = useRoomState( room, useCallback( (state: RoomState) => { - const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); + const content = state + ?.getStateEvents(EventType.RoomPowerLevels, "") + ?.getContent(); return [ content ?? {}, - content?.["events"] ?? {}, state?.maySendStateEvent(EventType.RoomPowerLevels, room.client.getSafeUserId()), - ]; + ] as const; }, [room.client], ), ); const [elementCallEnabled, setElementCallEnabled] = useState(() => { - return events[ElementCall.MEMBER_EVENT_TYPE.name] === 0; + return content.events?.[ElementCall.MEMBER_EVENT_TYPE.name] === 0; }); const onChange = useCallback( @@ -48,27 +50,24 @@ const ElementCallSwitch: React.FC = ({ room }) => { setElementCallEnabled(enabled); // Take a copy to avoid mutating the original - const newEvents = { ...events }; + const newContent = { events: {}, ...content }; if (enabled) { - const userLevel = newEvents[EventType.RoomMessage] ?? content.users_default ?? 0; + const userLevel = newContent.events[EventType.RoomMessage] ?? content.users_default ?? 0; const moderatorLevel = content.kick ?? 50; - newEvents[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel; - newEvents[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel; + newContent.events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel; + newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel; } else { - const adminLevel = newEvents[EventType.RoomPowerLevels] ?? content.state_default ?? 100; + const adminLevel = newContent.events[EventType.RoomPowerLevels] ?? content.state_default ?? 100; - newEvents[ElementCall.CALL_EVENT_TYPE.name] = adminLevel; - newEvents[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; + newContent.events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel; + newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; } - room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, { - events: newEvents, - ...content, - }); + room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); }, - [room.client, room.roomId, content, events, isPublic], + [room.client, room.roomId, content, isPublic], ); const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand; From 24d4ecf91296054bff4931195e91e1dbd0094239 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2024 12:40:01 +0000 Subject: [PATCH 10/13] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/accessibility/RovingTabIndex.tsx | 1 + .../structures/AutocompleteInput.tsx | 1 + src/components/structures/ContextMenu.tsx | 1 + src/components/structures/FilePanel.tsx | 4 +-- .../structures/NotificationPanel.tsx | 2 +- src/components/structures/RoomView.tsx | 5 ++- src/components/structures/ThreadPanel.tsx | 2 +- src/components/structures/ThreadView.tsx | 2 +- src/components/utils/Box.tsx | 35 +++++-------------- src/components/utils/Flex.tsx | 23 ++++++------ src/components/views/elements/Measured.tsx | 10 +++--- .../views/right_panel/TimelineCard.tsx | 2 +- .../views/rooms/ReadReceiptGroup.tsx | 1 + .../views/rooms/UserIdentityWarning.tsx | 10 +++--- .../wysiwyg_composer/EditWysiwygComposer.tsx | 2 +- .../wysiwyg_composer/SendWysiwygComposer.tsx | 5 ++- .../hooks/useComposerFunctions.ts | 1 + .../hooks/usePlainTextInitialization.ts | 1 + src/contexts/ScopedRoomContext.tsx | 11 +++--- src/hooks/useAsyncRefreshMemo.ts | 2 +- src/hooks/useRoomNotificationState.ts | 1 + src/hooks/useTransition.ts | 2 +- src/hooks/useUserOnboardingContext.ts | 1 + 23 files changed, 59 insertions(+), 66 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index d35a0291c3e..8fd8f90b9d4 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -392,6 +392,7 @@ export const useRovingTabIndex = ( }); }, []); // eslint-disable-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler const isActive = context.state.activeNode === nodeRef.current; return [onFocus, isActive, ref, nodeRef]; }; diff --git a/src/components/structures/AutocompleteInput.tsx b/src/components/structures/AutocompleteInput.tsx index 5e930d5f1b0..d2247a5c291 100644 --- a/src/components/structures/AutocompleteInput.tsx +++ b/src/components/structures/AutocompleteInput.tsx @@ -142,6 +142,7 @@ export const AutocompleteInput: React.FC = ({ {isFocused && suggestions.length ? (
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 2c7a864db5d..9ad05e4d44e 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -607,6 +607,7 @@ export const useContextMenu = (inputRef?: RefObject setIsOpen(false); }; + // eslint-disable-next-line react-compiler/react-compiler return [button.current ? isOpen : false, button, open, close, setIsOpen]; }; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 4c580cb9fe3..c2e6c1f38db 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -286,9 +286,7 @@ class FilePanel extends React.Component { ref={this.card} header={_t("right_panel|files_button")} > - {this.card.current && ( - - )} + - {this.card.current && } + {content} diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 772d5698a30..e2b5f8adce2 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -273,6 +273,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { } const onRetryClicked = (): void => { + // eslint-disable-next-line react-compiler/react-compiler room.state = LocalRoomState.NEW; defaultDispatcher.dispatch({ action: "local_room_event", @@ -2514,9 +2515,7 @@ export class RoomView extends React.Component { mainSplitContentClassName = "mx_MainSplit_timeline"; mainSplitBody = ( <> - {this.roomViewBody.current && ( - - )} + {auxPanel} {pinnedMessageBanner}
diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 0e82baa28b8..42528d319d2 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -204,7 +204,7 @@ const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => ref={card} closeButtonRef={closeButonRef} > - {card.current && } + {timelineSet ? ( { PosthogTrackers.trackInteraction("WebThreadViewBackButton", ev); }} > - {this.card.current && } +
{timeline}
{ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && ( diff --git a/src/components/utils/Box.tsx b/src/components/utils/Box.tsx index b48b13aa0fb..ecee4de141a 100644 --- a/src/components/utils/Box.tsx +++ b/src/components/utils/Box.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { useEffect, useRef } from "react"; +import React, { useMemo } from "react"; type FlexProps = { /** @@ -40,25 +40,6 @@ type FlexProps = { grow?: string | null; }; -/** - * Set or remove a CSS property - * @param ref the reference - * @param name the CSS property name - * @param value the CSS property value - */ -function addOrRemoveProperty( - ref: React.MutableRefObject, - name: string, - value?: string | null, -): void { - const style = ref.current!.style; - if (value) { - style.setProperty(name, value); - } else { - style.removeProperty(name); - } -} - /** * A flex child helper */ @@ -71,12 +52,12 @@ export function Box({ children, ...props }: React.PropsWithChildren): JSX.Element { - const ref = useRef(); - - useEffect(() => { - addOrRemoveProperty(ref, `--mx-box-flex`, flex); - addOrRemoveProperty(ref, `--mx-box-shrink`, shrink); - addOrRemoveProperty(ref, `--mx-box-grow`, grow); + const style = useMemo(() => { + const style: Record = {}; + if (flex) style["--mx-box-flex"] = flex; + if (shrink) style["--mx-box-shrink"] = shrink; + if (grow) style["--mx-box-grow"] = grow; + return style; }, [flex, grow, shrink]); return React.createElement( @@ -88,7 +69,7 @@ export function Box({ "mx_Box--shrink": !!shrink, "mx_Box--grow": !!grow, }), - ref, + style, }, children, ); diff --git a/src/components/utils/Flex.tsx b/src/components/utils/Flex.tsx index 852ebb7e40b..9d7db513118 100644 --- a/src/components/utils/Flex.tsx +++ b/src/components/utils/Flex.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import classNames from "classnames"; -import React, { useEffect, useRef } from "react"; +import React, { useMemo } from "react"; type FlexProps = { /** @@ -64,15 +64,16 @@ export function Flex({ children, ...props }: React.PropsWithChildren): JSX.Element { - const ref = useRef(); + const style = useMemo( + () => ({ + "--mx-flex-display": display, + "--mx-flex-direction": direction, + "--mx-flex-align": align, + "--mx-flex-justify": justify, + "--mx-flex-gap": gap, + }), + [align, direction, display, gap, justify], + ); - useEffect(() => { - ref.current!.style.setProperty(`--mx-flex-display`, display); - ref.current!.style.setProperty(`--mx-flex-direction`, direction); - ref.current!.style.setProperty(`--mx-flex-align`, align); - ref.current!.style.setProperty(`--mx-flex-justify`, justify); - ref.current!.style.setProperty(`--mx-flex-gap`, gap); - }, [align, direction, display, gap, justify]); - - return React.createElement(as, { ...props, className: classNames("mx_Flex", className), ref }, children); + return React.createElement(as, { ...props, className: classNames("mx_Flex", className), style }, children); } diff --git a/src/components/views/elements/Measured.tsx b/src/components/views/elements/Measured.tsx index e2f9d14ddee..640387784dc 100644 --- a/src/components/views/elements/Measured.tsx +++ b/src/components/views/elements/Measured.tsx @@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { RefObject } from "react"; import UIStore, { UI_EVENTS } from "../../../stores/UIStore"; interface IProps { - sensor: Element; + sensor: RefObject; breakpoint: number; onMeasurement(narrow: boolean): void; } @@ -35,14 +35,14 @@ export default class Measured extends React.PureComponent { } public componentDidUpdate(prevProps: Readonly): void { - const previous = prevProps.sensor; - const current = this.props.sensor; + const previous = prevProps.sensor.current; + const current = this.props.sensor.current; if (previous === current) return; if (previous) { UIStore.instance.stopTrackingElementDimensions(`Measured${this.instanceId}`); } if (current) { - UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, this.props.sensor); + UIStore.instance.trackElementDimensions(`Measured${this.instanceId}`, this.props.sensor.current); } } diff --git a/src/components/views/right_panel/TimelineCard.tsx b/src/components/views/right_panel/TimelineCard.tsx index f62319f3cda..127abf53415 100644 --- a/src/components/views/right_panel/TimelineCard.tsx +++ b/src/components/views/right_panel/TimelineCard.tsx @@ -213,7 +213,7 @@ export default class TimelineCard extends React.Component { header={_t("right_panel|video_room_chat|title")} ref={this.card} > - {this.card.current && } +
{jumpToBottom} = ({ room } initialisedRef.current = InitialisationStatus.Completed; }, [crypto, room, addMembersWhoNeedApproval, updateCurrentPrompt]); - loadMembers().catch((e) => { - logger.error("Error initialising UserIdentityWarning:", e); - }); + useEffect(() => { + loadMembers().catch((e) => { + logger.error("Error initialising UserIdentityWarning:", e); + }); + }, [loadMembers]); // When a user's verification status changes, we check if they need to be // added/removed from the set of members needing approval. diff --git a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx index a28d2746463..c2a24aaf105 100644 --- a/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/EditWysiwygComposer.tsx @@ -44,7 +44,7 @@ export default function EditWysiwygComposer({ className, ...props }: EditWysiwygComposerProps): JSX.Element { - const defaultContextValue = useMemo(() => getDefaultContextValue({ editorStateTransfer }), []); + const defaultContextValue = useMemo(() => getDefaultContextValue({ editorStateTransfer }), [editorStateTransfer]); const initialContent = useInitialContent(editorStateTransfer); const isReady = !editorStateTransfer || initialContent !== undefined; diff --git a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx index ff74731cf7c..d7a6d8b425c 100644 --- a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx @@ -52,7 +52,10 @@ export default function SendWysiwygComposer({ ...props }: SendWysiwygComposerProps): JSX.Element { const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer; - const defaultContextValue = useMemo(() => getDefaultContextValue({ eventRelation: props.eventRelation }), []); + const defaultContextValue = useMemo( + () => getDefaultContextValue({ eventRelation: props.eventRelation }), + [props.eventRelation], + ); return ( diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts index aa7c672af3a..2d26299fd83 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts @@ -21,6 +21,7 @@ export function useComposerFunctions( () => ({ clear: () => { if (ref.current) { + // eslint-disable-next-line react-compiler/react-compiler ref.current.innerHTML = ""; } }, diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts index 6adf43d9a42..812cd57c516 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextInitialization.ts @@ -12,6 +12,7 @@ export function usePlainTextInitialization(initialContent = "", ref: RefObject { // always read and write the ref.current using .innerHTML for consistency in linebreak and HTML entity handling if (ref.current) { + // eslint-disable-next-line react-compiler/react-compiler ref.current.innerHTML = initialContent; } }, [ref, initialContent]); diff --git a/src/contexts/ScopedRoomContext.tsx b/src/contexts/ScopedRoomContext.tsx index 1222443d290..032e98afd26 100644 --- a/src/contexts/ScopedRoomContext.tsx +++ b/src/contexts/ScopedRoomContext.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; -import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useRef, useState } from "react"; +import React, { ContextType, createContext, memo, ReactNode, useContext, useEffect, useMemo, useState } from "react"; import { objectKeyChanges } from "../utils/objects.ts"; import { useTypedEventEmitter } from "../hooks/useEventEmitter.ts"; @@ -48,15 +48,16 @@ const ScopedRoomContext = createContext | undefin // Uses react memo and leverages splatting the value to ensure that the context is only updated when the state changes (shallow compare) export const ScopedRoomContextProvider = memo( ({ children, ...state }: { children: ReactNode } & ContextValue): JSX.Element => { - const contextRef = useRef(new EfficientContext(state)); + // eslint-disable-next-line react-compiler/react-compiler,react-hooks/exhaustive-deps + const context = useMemo(() => new EfficientContext(state), []); useEffect(() => { - contextRef.current.setState(state); - }, [state]); + context.setState(state); + }, [context, state]); // Includes the legacy RoomContext provider for backwards compatibility with class components return ( - {children} + {children} ); }, diff --git a/src/hooks/useAsyncRefreshMemo.ts b/src/hooks/useAsyncRefreshMemo.ts index 122b7cdb20d..694678ea75a 100644 --- a/src/hooks/useAsyncRefreshMemo.ts +++ b/src/hooks/useAsyncRefreshMemo.ts @@ -34,7 +34,7 @@ export function useAsyncRefreshMemo(fn: Fn, deps: DependencyList, initialV return () => { discard = true; }; - }, deps); // eslint-disable-line react-hooks/exhaustive-deps + }, deps); // eslint-disable-line react-hooks/exhaustive-deps,react-compiler/react-compiler useEffect(refresh, [refresh]); return [value, refresh]; } diff --git a/src/hooks/useRoomNotificationState.ts b/src/hooks/useRoomNotificationState.ts index 46dd4e5524e..dec864c984a 100644 --- a/src/hooks/useRoomNotificationState.ts +++ b/src/hooks/useRoomNotificationState.ts @@ -25,6 +25,7 @@ export const useNotificationState = (room: Room): [RoomNotifState | undefined, ( setNotificationState(echoChamber.notificationVolume); } }); + // eslint-disable-next-line react-compiler/react-compiler const setter = useCallback((state: RoomNotifState) => (echoChamber.notificationVolume = state), [echoChamber]); return [notificationState, setter]; }; diff --git a/src/hooks/useTransition.ts b/src/hooks/useTransition.ts index 853354e1001..f1c6ab147e9 100644 --- a/src/hooks/useTransition.ts +++ b/src/hooks/useTransition.ts @@ -22,6 +22,6 @@ export const useTransition = (callback: (...params: D) useEffect(() => { if (args.current !== null) func.current(...args.current); args.current = deps; - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler,react-hooks/exhaustive-deps }, deps); }; diff --git a/src/hooks/useUserOnboardingContext.ts b/src/hooks/useUserOnboardingContext.ts index 8bd93603d44..47baac03603 100644 --- a/src/hooks/useUserOnboardingContext.ts +++ b/src/hooks/useUserOnboardingContext.ts @@ -35,6 +35,7 @@ const USER_ONBOARDING_CONTEXT_INTERVAL = 5000; */ function useRefOf(value: (...values: T) => R): (...values: T) => R { const ref = useRef(value); + // eslint-disable-next-line react-compiler/react-compiler ref.current = value; return useCallback((...values: T) => ref.current(...values), []); } From ba71ce126ed17b27d831165a34482400fc87d94c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jan 2025 09:56:51 +0000 Subject: [PATCH 11/13] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../memberlist/MemberListViewModel.tsx | 20 ++++----- src/utils/location/useMap.ts | 43 +++++++++++-------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/components/viewmodels/memberlist/MemberListViewModel.tsx b/src/components/viewmodels/memberlist/MemberListViewModel.tsx index 4a1a2d59f19..06daedc2c9f 100644 --- a/src/components/viewmodels/memberlist/MemberListViewModel.tsx +++ b/src/components/viewmodels/memberlist/MemberListViewModel.tsx @@ -19,7 +19,7 @@ import { UserEvent, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { throttle } from "lodash"; import { RoomMember } from "../../../models/rooms/RoomMember"; @@ -122,9 +122,9 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { const [isLoading, setIsLoading] = useState(true); // This is the last known total number of members in this room. - const totalMemberCount = useRef(0); + const [totalMemberCount, setTotalMemberCount] = useState(0); - const searchQuery = useRef(""); + const [searchQuery, setSearchQuery] = useState(""); const loadMembers = useMemo( () => @@ -132,7 +132,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { async (): Promise => { const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList( roomId, - searchQuery.current, + searchQuery, ); const newMemberMap = new Map(); // First add the invited room members @@ -141,7 +141,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { newMemberMap.set(member.userId, roomMember); } // Then add the third party invites - const threePidInvited = getPending3PidInvites(room, searchQuery.current); + const threePidInvited = getPending3PidInvites(room, searchQuery); for (const invited of threePidInvited) { const key = invited.threePidInvite!.event.getContent().display_name; newMemberMap.set(key, invited); @@ -152,23 +152,23 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { newMemberMap.set(member.userId, roomMember); } setMemberMap(newMemberMap); - if (!searchQuery.current) { + if (!searchQuery) { /** * Since searching for members only gives you the relevant * members matching the query, do not update the totalMemberCount! **/ - totalMemberCount.current = newMemberMap.size; + setTotalMemberCount(newMemberMap.size); } }, 500, { leading: true, trailing: true }, ), - [roomId, sdkContext.memberListStore, room], + [sdkContext.memberListStore, roomId, searchQuery, room], ); const search = useCallback( (query: string) => { - searchQuery.current = query; + setSearchQuery(query); loadMembers(); }, [loadMembers], @@ -257,7 +257,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { isPresenceEnabled, isLoading, onInviteButtonClick, - shouldShowSearch: totalMemberCount.current >= 20, + shouldShowSearch: totalMemberCount >= 20, canInvite, }; } diff --git a/src/utils/location/useMap.ts b/src/utils/location/useMap.ts index ebb24fe8be9..4a299101bd5 100644 --- a/src/utils/location/useMap.ts +++ b/src/utils/location/useMap.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { useEffect, useMemo } from "react"; +import { useEffect, useState } from "react"; import type { Map as MapLibreMap } from "maplibre-gl"; import { createMap } from "./map"; @@ -26,25 +26,30 @@ interface UseMapProps { */ export const useMap = ({ interactive, bodyId, onError }: UseMapProps): MapLibreMap | undefined => { const cli = useMatrixClientContext(); - - const map = useMemo(() => { - try { - return createMap(cli, !!interactive, bodyId, onError); - } catch (error) { - console.error("Error encountered in useMap", error); - if (error instanceof Error) { - onError?.(error); + const [map, setMap] = useState(); + + useEffect( + () => { + let map: MapLibreMap | undefined; + try { + map = createMap(cli, !!interactive, bodyId, onError); + setMap(map); + } catch (error) { + console.error("Error encountered in useMap", error); + if (error instanceof Error) { + onError?.(error); + } } - } - }, [bodyId, cli, interactive, onError]); - - // cleanup - useEffect(() => { - if (!map) return; - return () => { - map.remove(); - }; - }, [map]); + return () => { + if (map) { + map.remove(); + setMap(undefined); + } + }; + }, + // map is excluded as a dependency + [cli, interactive, bodyId, onError], + ); return map; }; From a052509db1bc278ef824966092e439340100f180 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 16 Jan 2025 11:40:11 +0000 Subject: [PATCH 12/13] Revert MemberListViewModel.tsx changes and disable linter per line Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../memberlist/MemberListViewModel.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/viewmodels/memberlist/MemberListViewModel.tsx b/src/components/viewmodels/memberlist/MemberListViewModel.tsx index 06daedc2c9f..ba6ad42f5f0 100644 --- a/src/components/viewmodels/memberlist/MemberListViewModel.tsx +++ b/src/components/viewmodels/memberlist/MemberListViewModel.tsx @@ -19,7 +19,7 @@ import { UserEvent, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; -import { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { throttle } from "lodash"; import { RoomMember } from "../../../models/rooms/RoomMember"; @@ -122,17 +122,18 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { const [isLoading, setIsLoading] = useState(true); // This is the last known total number of members in this room. - const [totalMemberCount, setTotalMemberCount] = useState(0); + const totalMemberCount = useRef(0); - const [searchQuery, setSearchQuery] = useState(""); + const searchQuery = useRef(""); const loadMembers = useMemo( () => throttle( + // eslint-disable-next-line react-compiler/react-compiler async (): Promise => { const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList( roomId, - searchQuery, + searchQuery.current, ); const newMemberMap = new Map(); // First add the invited room members @@ -141,7 +142,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { newMemberMap.set(member.userId, roomMember); } // Then add the third party invites - const threePidInvited = getPending3PidInvites(room, searchQuery); + const threePidInvited = getPending3PidInvites(room, searchQuery.current); for (const invited of threePidInvited) { const key = invited.threePidInvite!.event.getContent().display_name; newMemberMap.set(key, invited); @@ -152,23 +153,23 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { newMemberMap.set(member.userId, roomMember); } setMemberMap(newMemberMap); - if (!searchQuery) { + if (!searchQuery.current) { /** * Since searching for members only gives you the relevant * members matching the query, do not update the totalMemberCount! **/ - setTotalMemberCount(newMemberMap.size); + totalMemberCount.current = newMemberMap.size; } }, 500, { leading: true, trailing: true }, ), - [sdkContext.memberListStore, roomId, searchQuery, room], + [roomId, sdkContext.memberListStore, room], ); const search = useCallback( (query: string) => { - setSearchQuery(query); + searchQuery.current = query; loadMembers(); }, [loadMembers], @@ -257,7 +258,8 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { isPresenceEnabled, isLoading, onInviteButtonClick, - shouldShowSearch: totalMemberCount >= 20, + // eslint-disable-next-line react-compiler/react-compiler + shouldShowSearch: totalMemberCount.current >= 20, canInvite, }; } From 83ef292d03a9366180b549361e764a00a17268a7 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 16 Jan 2025 17:44:59 +0530 Subject: [PATCH 13/13] Make viewmodel compatible with react-compiler linter - Remove searchQuery ref/state and instead pass this query to the loadMember function. - Now we no longer need a separate search function --- .../memberlist/MemberListViewModel.tsx | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/components/viewmodels/memberlist/MemberListViewModel.tsx b/src/components/viewmodels/memberlist/MemberListViewModel.tsx index ba6ad42f5f0..88eacb1b931 100644 --- a/src/components/viewmodels/memberlist/MemberListViewModel.tsx +++ b/src/components/viewmodels/memberlist/MemberListViewModel.tsx @@ -19,7 +19,7 @@ import { UserEvent, } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { throttle } from "lodash"; import { RoomMember } from "../../../models/rooms/RoomMember"; @@ -120,20 +120,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { const sdkContext = useContext(SDKContext); const [memberMap, setMemberMap] = useState>(new Map()); const [isLoading, setIsLoading] = useState(true); - // This is the last known total number of members in this room. - const totalMemberCount = useRef(0); - - const searchQuery = useRef(""); + const [totalMemberCount, setTotalMemberCount] = useState(0); const loadMembers = useMemo( () => throttle( - // eslint-disable-next-line react-compiler/react-compiler - async (): Promise => { + async (searchQuery?: string): Promise => { const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList( roomId, - searchQuery.current, + searchQuery, ); const newMemberMap = new Map(); // First add the invited room members @@ -142,7 +138,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { newMemberMap.set(member.userId, roomMember); } // Then add the third party invites - const threePidInvited = getPending3PidInvites(room, searchQuery.current); + const threePidInvited = getPending3PidInvites(room, searchQuery); for (const invited of threePidInvited) { const key = invited.threePidInvite!.event.getContent().display_name; newMemberMap.set(key, invited); @@ -153,26 +149,18 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { newMemberMap.set(member.userId, roomMember); } setMemberMap(newMemberMap); - if (!searchQuery.current) { + if (!searchQuery) { /** * Since searching for members only gives you the relevant * members matching the query, do not update the totalMemberCount! **/ - totalMemberCount.current = newMemberMap.size; + setTotalMemberCount(newMemberMap.size); } }, 500, { leading: true, trailing: true }, ), - [roomId, sdkContext.memberListStore, room], - ); - - const search = useCallback( - (query: string) => { - searchQuery.current = query; - loadMembers(); - }, - [loadMembers], + [sdkContext.memberListStore, roomId, room], ); const isPresenceEnabled = useMemo( @@ -253,13 +241,12 @@ export function useMemberListViewModel(roomId: string): MemberListViewState { return { members: Array.from(memberMap.values()), - search, + search: loadMembers, shouldShowInvite, isPresenceEnabled, isLoading, onInviteButtonClick, - // eslint-disable-next-line react-compiler/react-compiler - shouldShowSearch: totalMemberCount.current >= 20, + shouldShowSearch: totalMemberCount >= 20, canInvite, }; }