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 1/9] 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 b6ce541590..ee47094cf9 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 403b6349c9..83a00c122d 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 6f7cf79598..0000000000 --- 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 2/9] 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 2b0dd2c186..e302a52673 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 5a75055510..902852a8ef 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 551c5b4d6e..6ed22a8c2e 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 3/9] 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 3e5a5ead60..ad7d9c825e 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 4/9] 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 e302a52673..a017112b4e 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 5/9] 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 82146bcc5e..9e6263bcfb 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 98597c7360..a28d274646 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 83f17a2f7b..3355913abd 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 4ae4875c96..e9bd392a60 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 6/9] 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 783ea1bce3..a428b7a148 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 7/9] 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 | 40 +++++++++---------- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/components/views/spaces/QuickThemeSwitcher.tsx b/src/components/views/spaces/QuickThemeSwitcher.tsx index 195fcb9899..f4c229ae04 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 110c9d51f8..1ea10bed68 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 308aedc205..300a15a4ec 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 8/9] 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 c410e4f24c..b47d4c02f8 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 9/9] 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 a428b7a148..14de26629b 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;