From 4160a33edbc688fe299aec685c8afb7fb9bba990 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 22 Dec 2024 14:54:57 +0200 Subject: [PATCH] web/all: remove unnecessary uses of useCallback --- web/src/ui/composer/Autocompleter.tsx | 4 +- web/src/ui/composer/ComposerMedia.tsx | 63 ++++++ web/src/ui/composer/MessageComposer.tsx | 190 +++++++----------- web/src/ui/emojipicker/EmojiGroup.tsx | 21 +- web/src/ui/emojipicker/EmojiPicker.tsx | 14 +- web/src/ui/emojipicker/GIFPicker.tsx | 12 +- web/src/ui/emojipicker/StickerPicker.tsx | 10 +- web/src/ui/login/BeeperLogin.tsx | 19 +- web/src/ui/login/LoginScreen.tsx | 23 +-- web/src/ui/login/VerificationScreen.tsx | 6 +- web/src/ui/modal/Modal.tsx | 4 +- web/src/ui/rightpanel/MemberList.tsx | 13 +- web/src/ui/rightpanel/UserInfoDeviceList.tsx | 14 +- web/src/ui/roomlist/RoomList.tsx | 14 +- web/src/ui/roomview/RoomPreview.tsx | 10 +- web/src/ui/roomview/RoomViewHeader.tsx | 6 +- web/src/ui/settings/SettingsView.tsx | 61 +++--- web/src/ui/timeline/TimelineEvent.tsx | 6 +- web/src/ui/timeline/TimelineView.tsx | 4 +- .../timeline/menu/ConfirmWithMessageModal.tsx | 19 +- web/src/ui/timeline/menu/usePrimaryItems.tsx | 22 +- .../ui/timeline/menu/useSecondaryItems.tsx | 35 ++-- web/src/ui/util/ResizeHandle.tsx | 5 +- 23 files changed, 285 insertions(+), 290 deletions(-) create mode 100644 web/src/ui/composer/ComposerMedia.tsx diff --git a/web/src/ui/composer/Autocompleter.tsx b/web/src/ui/composer/Autocompleter.tsx index 78e1a0d..432d327 100644 --- a/web/src/ui/composer/Autocompleter.tsx +++ b/web/src/ui/composer/Autocompleter.tsx @@ -80,13 +80,13 @@ function useAutocompleter({ }) document.querySelector(`div.autocompletion-item[data-index='${index}']`)?.scrollIntoView({ block: "nearest" }) }) - const onClick = useEvent((evt: React.MouseEvent) => { + const onClick = (evt: React.MouseEvent) => { const idx = evt.currentTarget.getAttribute("data-index") if (idx) { onSelect(+idx) setAutocomplete(null) } - }) + } useEffect(() => { if (params.selected !== undefined) { onSelect(params.selected) diff --git a/web/src/ui/composer/ComposerMedia.tsx b/web/src/ui/composer/ComposerMedia.tsx new file mode 100644 index 0000000..44fe952 --- /dev/null +++ b/web/src/ui/composer/ComposerMedia.tsx @@ -0,0 +1,63 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import Client from "@/api/client.ts" +import { RoomStateStore, usePreference } from "@/api/statestore" +import type { MediaMessageEventContent } from "@/api/types" +import { LeafletPicker } from "../maps/async.tsx" +import { useMediaContent } from "../timeline/content/useMediaContent.tsx" +import CloseIcon from "@/icons/close.svg?react" +import "./MessageComposer.css" + +export interface ComposerMediaProps { + content: MediaMessageEventContent + clearMedia: false | (() => void) +} + +export const ComposerMedia = ({ content, clearMedia }: ComposerMediaProps) => { + const [mediaContent, containerClass, containerStyle] = useMediaContent( + content, "m.room.message", { height: 120, width: 360 }, + ) + return
+
+ {mediaContent} +
+ {clearMedia && } +
+} + +export interface ComposerLocationValue { + lat: number + long: number + prec?: number +} + +export interface ComposerLocationProps { + room: RoomStateStore + client: Client + location: ComposerLocationValue + onChange: (location: ComposerLocationValue) => void + clearLocation: () => void +} + +export const ComposerLocation = ({ client, room, location, onChange, clearLocation }: ComposerLocationProps) => { + const tileTemplate = usePreference(client.store, room, "leaflet_tile_template") + return
+
+ +
+ +
+} diff --git a/web/src/ui/composer/MessageComposer.tsx b/web/src/ui/composer/MessageComposer.tsx index 55775d7..701b07d 100644 --- a/web/src/ui/composer/MessageComposer.tsx +++ b/web/src/ui/composer/MessageComposer.tsx @@ -15,8 +15,7 @@ // along with this program. If not, see . import React, { CSSProperties, use, useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from "react" import { ScaleLoader } from "react-spinners" -import Client from "@/api/client.ts" -import { RoomStateStore, usePreference, useRoomEvent } from "@/api/statestore" +import { useRoomEvent } from "@/api/statestore" import type { EventID, MediaMessageEventContent, @@ -29,21 +28,18 @@ import type { import { PartialEmoji, emojiToMarkdown } from "@/util/emoji" import { isMobileDevice } from "@/util/ismobile.ts" import { escapeMarkdown } from "@/util/markdown.ts" -import useEvent from "@/util/useEvent.ts" import ClientContext from "../ClientContext.ts" import EmojiPicker from "../emojipicker/EmojiPicker.tsx" import GIFPicker from "../emojipicker/GIFPicker.tsx" import StickerPicker from "../emojipicker/StickerPicker.tsx" import { keyToString } from "../keybindings.ts" -import { LeafletPicker } from "../maps/async.tsx" import { ModalContext } from "../modal/Modal.tsx" import { useRoomContext } from "../roomview/roomcontext.ts" import { ReplyBody } from "../timeline/ReplyBody.tsx" -import { useMediaContent } from "../timeline/content/useMediaContent.tsx" import type { AutocompleteQuery } from "./Autocompleter.tsx" +import { ComposerLocation, ComposerLocationValue, ComposerMedia } from "./ComposerMedia.tsx" import { charToAutocompleteType, emojiQueryRegex, getAutocompleter } from "./getAutocompleter.ts" import AttachIcon from "@/icons/attach.svg?react" -import CloseIcon from "@/icons/close.svg?react" import EmojiIcon from "@/icons/emoji-categories/smileys-emotion.svg?react" import GIFIcon from "@/icons/gif.svg?react" import LocationIcon from "@/icons/location.svg?react" @@ -52,12 +48,6 @@ import SendIcon from "@/icons/send.svg?react" import StickerIcon from "@/icons/sticker.svg?react" import "./MessageComposer.css" -export interface ComposerLocationValue { - lat: number - long: number - prec?: number -} - export interface ComposerState { text: string media: MediaMessageEventContent | null @@ -174,13 +164,13 @@ const MessageComposer = () => { textInput.current?.focus() }, [room.roomID]) const canSend = Boolean(state.text || state.media || state.location) - const sendMessage = useEvent((evt: React.FormEvent) => { + const onClickSend = (evt: React.FormEvent) => { evt.preventDefault() if (!canSend) { return } doSendMessage(state) - }) + } const doSendMessage = (state: ComposerState) => { if (editing) { setState(draftStore.get(room.roomID) ?? emptyComposer) @@ -245,7 +235,7 @@ const MessageComposer = () => { mentions, }).catch(err => window.alert("Failed to send message: " + err)) } - const onComposerCaretChange = useEvent((evt: CaretEvent, newText?: string) => { + const onComposerCaretChange = (evt: CaretEvent, newText?: string) => { const area = evt.currentTarget if (area.selectionStart <= (autocomplete?.startPos ?? 0)) { if (autocomplete) { @@ -281,8 +271,8 @@ const MessageComposer = () => { }) } } - }) - const onComposerKeyDown = useEvent((evt: React.KeyboardEvent) => { + } + const onComposerKeyDown = (evt: React.KeyboardEvent) => { const inp = evt.currentTarget const fullKey = keyToString(evt) const sendKey = fullKey === "Enter" || fullKey === "Ctrl+Enter" @@ -295,7 +285,7 @@ const MessageComposer = () => { || autocomplete.selected !== undefined || !document.getElementById("composer-autocompletions")?.classList.contains("has-items") )) { - sendMessage(evt) + onClickSend(evt) } else if (autocomplete) { let autocompleteUpdate: Partial | null | undefined if (fullKey === "Tab" || fullKey === "ArrowDown") { @@ -340,8 +330,8 @@ const MessageComposer = () => { evt.stopPropagation() roomCtx.setEditing(null) } - }) - const onChange = useEvent((evt: React.ChangeEvent) => { + } + const onChange = (evt: React.ChangeEvent) => { setState({ text: evt.target.value }) const now = Date.now() if (evt.target.value !== "" && typingSentAt.current + 5_000 < now) { @@ -358,7 +348,7 @@ const MessageComposer = () => { } } onComposerCaretChange(evt, evt.target.value) - }) + } const doUploadFile = useCallback((file: File | null | undefined) => { if (!file) { return @@ -380,10 +370,7 @@ const MessageComposer = () => { .catch(err => window.alert("Failed to upload file: " + err)) .finally(() => setLoadingMedia(false)) }, [room]) - const onAttachFile = useEvent( - (evt: React.ChangeEvent) => doUploadFile(evt.target.files?.[0]), - ) - const onPaste = useEvent((evt: React.ClipboardEvent) => { + const onPaste = (evt: React.ClipboardEvent) => { const file = evt.clipboardData?.files?.[0] const text = evt.clipboardData.getData("text/plain") const input = evt.currentTarget @@ -400,7 +387,7 @@ const MessageComposer = () => { return } evt.preventDefault() - }) + } // To ensure the cursor jumps to the end, do this in an effect rather than as the initial value of useState // To try to avoid the input bar flashing, use useLayoutEffect instead of useEffect useLayoutEffect(() => { @@ -450,7 +437,6 @@ const MessageComposer = () => { draftStore.set(room.roomID, state) } }, [roomCtx, room, state, editing]) - const openFilePicker = useCallback(() => fileInput.current!.click(), []) const clearMedia = useCallback(() => setState({ media: null, location: null }), []) const onChangeLocation = useCallback((location: ComposerLocationValue) => setState({ location }), []) const closeReply = useCallback((evt: React.MouseEvent) => { @@ -461,56 +447,6 @@ const MessageComposer = () => { evt.stopPropagation() roomCtx.setEditing(null) }, [roomCtx]) - const getEmojiPickerStyle = () => ({ - bottom: (composerRef.current?.clientHeight ?? 32) + 4 + 24, - right: "var(--timeline-horizontal-padding)", - }) - const openEmojiPicker = useEvent(() => { - openModal({ - content: { - const mdEmoji = emojiToMarkdown(emoji) - setState({ - text: state.text.slice(0, textInput.current?.selectionStart ?? 0) - + mdEmoji - + state.text.slice(textInput.current?.selectionEnd ?? 0), - }) - if (textInput.current) { - textInput.current.setSelectionRange(textInput.current.selectionStart + mdEmoji.length, 0) - } - }} - // TODO allow keeping open on select on non-mobile devices - // (requires onSelect to be able to keep track of the state after updating it) - closeOnSelect={true} - />, - onClose: () => !isMobileDevice && textInput.current?.focus(), - }) - }) - const openGIFPicker = useEvent(() => { - openModal({ - content: setState({ media })} - />, - onClose: () => !isMobileDevice && textInput.current?.focus(), - }) - }) - const openStickerPicker = useEvent(() => { - openModal({ - content: doSendMessage({ ...state, media, text: "" })} - />, - onClose: () => !isMobileDevice && textInput.current?.focus(), - }) - }) - const openLocationPicker = useEvent(() => { - setState({ location: { lat: 0, long: 0, prec: 1 }, media: null }) - }) const Autocompleter = getAutocompleter(autocomplete, client, room) let mediaDisabledTitle: string | undefined let stickerDisabledTitle: string | undefined @@ -533,7 +469,57 @@ const MessageComposer = () => { } else if (state.text && !editing) { stickerDisabledTitle = "You can't attach a sticker to a message with text" } + const getEmojiPickerStyle = () => ({ + bottom: (composerRef.current?.clientHeight ?? 32) + 4 + 24, + right: "var(--timeline-horizontal-padding)", + }) const makeAttachmentButtons = (includeText = false) => { + const openEmojiPicker = () => { + openModal({ + content: { + const mdEmoji = emojiToMarkdown(emoji) + setState({ + text: state.text.slice(0, textInput.current?.selectionStart ?? 0) + + mdEmoji + + state.text.slice(textInput.current?.selectionEnd ?? 0), + }) + if (textInput.current) { + textInput.current.setSelectionRange(textInput.current.selectionStart + mdEmoji.length, 0) + } + }} + // TODO allow keeping open on select on non-mobile devices + // (requires onSelect to be able to keep track of the state after updating it) + closeOnSelect={true} + />, + onClose: () => !isMobileDevice && textInput.current?.focus(), + }) + } + const openGIFPicker = () => { + openModal({ + content: setState({ media })} + />, + onClose: () => !isMobileDevice && textInput.current?.focus(), + }) + } + const openStickerPicker = () => { + openModal({ + content: doSendMessage({ ...state, media, text: "" })} + />, + onClose: () => !isMobileDevice && textInput.current?.focus(), + }) + } + const openLocationPicker = () => { + setState({ location: { lat: 0, long: 0, prec: 1 }, media: null }) + } return <> } - const openButtonsModal = useEvent(() => { + const openButtonsModal = () => { const style: CSSProperties = getEmojiPickerStyle() style.left = style.right delete style.right @@ -571,7 +557,7 @@ const MessageComposer = () => { {makeAttachmentButtons(true)} , }) - }) + } const inlineButtons = state.text === "" || window.innerWidth > 720 const showSendButton = canSend || window.innerWidth > 720 const disableClearMedia = editing && state.media?.msgtype === "m.sticker" @@ -625,49 +611,19 @@ const MessageComposer = () => { /> {inlineButtons && makeAttachmentButtons()} {showSendButton && } - + doUploadFile(evt.target.files?.[0])} + type="file" + value="" + /> } -interface ComposerMediaProps { - content: MediaMessageEventContent - clearMedia: false | (() => void) -} - -const ComposerMedia = ({ content, clearMedia }: ComposerMediaProps) => { - const [mediaContent, containerClass, containerStyle] = useMediaContent( - content, "m.room.message", { height: 120, width: 360 }, - ) - return
-
- {mediaContent} -
- {clearMedia && } -
-} - -interface ComposerLocationProps { - room: RoomStateStore - client: Client - location: ComposerLocationValue - onChange: (location: ComposerLocationValue) => void - clearLocation: () => void -} - -const ComposerLocation = ({ client, room, location, onChange, clearLocation }: ComposerLocationProps) => { - const tileTemplate = usePreference(client.store, room, "leaflet_tile_template") - return
-
- -
- -
-} - export default MessageComposer diff --git a/web/src/ui/emojipicker/EmojiGroup.tsx b/web/src/ui/emojipicker/EmojiGroup.tsx index da3ed3a..dfb993a 100644 --- a/web/src/ui/emojipicker/EmojiGroup.tsx +++ b/web/src/ui/emojipicker/EmojiGroup.tsx @@ -13,11 +13,10 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { use, useCallback } from "react" +import React, { use } from "react" import { stringToRoomStateGUID } from "@/api/types" import useContentVisibility from "@/util/contentvisibility.ts" import { CATEGORY_FREQUENTLY_USED, CustomEmojiPack, Emoji, categories } from "@/util/emoji" -import useEvent from "@/util/useEvent.ts" import ClientContext from "../ClientContext.ts" import renderEmoji from "./renderEmoji.tsx" @@ -56,27 +55,25 @@ export const EmojiGroup = ({ } return emoji } - const onClickEmoji = useEvent((evt: React.MouseEvent) => - onSelect(getEmojiFromAttrs(evt.currentTarget))) - const onMouseOverEmoji = useEvent((evt: React.MouseEvent) => - setPreviewEmoji?.(getEmojiFromAttrs(evt.currentTarget))) - const onMouseOutEmoji = useCallback(() => setPreviewEmoji?.(undefined), [setPreviewEmoji]) - const onClickSubscribePack = useEvent((evt: React.MouseEvent) => { + const onMouseOverEmoji = setPreviewEmoji && ((evt: React.MouseEvent) => + setPreviewEmoji(getEmojiFromAttrs(evt.currentTarget))) + const onMouseOutEmoji = setPreviewEmoji && (() => setPreviewEmoji(undefined)) + const onClickSubscribePack = (evt: React.MouseEvent) => { const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id")) if (!guid) { return } client.subscribeToEmojiPack(guid, true) .catch(err => window.alert(`Failed to subscribe to emoji pack: ${err}`)) - }) - const onClickUnsubscribePack = useEvent((evt: React.MouseEvent) => { + } + const onClickUnsubscribePack = (evt: React.MouseEvent) => { const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id")) if (!guid) { return } client.subscribeToEmojiPack(guid, false) .catch(err => window.alert(`Failed to unsubscribe from emoji pack: ${err}`)) - }) + } let categoryName: string if (typeof categoryID === "number") { @@ -112,7 +109,7 @@ export const EmojiGroup = ({ data-emoji-index={idx} onMouseOver={onMouseOverEmoji} onMouseOut={onMouseOutEmoji} - onClick={onClickEmoji} + onClick={evt => onSelect(getEmojiFromAttrs(evt.currentTarget))} title={emoji.t} >{renderEmoji(emoji)}) : null} diff --git a/web/src/ui/emojipicker/EmojiPicker.tsx b/web/src/ui/emojipicker/EmojiPicker.tsx index 0a96251..940d371 100644 --- a/web/src/ui/emojipicker/EmojiPicker.tsx +++ b/web/src/ui/emojipicker/EmojiPicker.tsx @@ -19,7 +19,6 @@ import { RoomStateStore, useCustomEmojis } from "@/api/statestore" import { roomStateGUIDToString } from "@/api/types" import { CATEGORY_FREQUENTLY_USED, Emoji, PartialEmoji, categories, useFilteredEmojis } from "@/util/emoji" import { isMobileDevice } from "@/util/ismobile.ts" -import useEvent from "@/util/useEvent.ts" import ClientContext from "../ClientContext.ts" import { ModalCloseContext } from "../modal/Modal.tsx" import { EmojiGroup } from "./EmojiGroup.tsx" @@ -72,7 +71,6 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe frequentlyUsed: client.store.frequentlyUsedEmoji, customEmojiPacks, }) - const clearQuery = useCallback(() => setQuery(""), []) const close = closeOnSelect ? use(ModalCloseContext) : null const onSelectWrapped = useCallback((emoji?: PartialEmoji) => { if (!emoji) { @@ -85,12 +83,10 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe } close?.() }, [onSelect, selected, client, close]) - const onClickFreeformReact = useEvent(() => onSelectWrapped({ u: query })) - const onChangeQuery = useCallback((evt: React.ChangeEvent) => setQuery(evt.target.value), []) - const onClickCategoryButton = useCallback((evt: React.MouseEvent) => { + const onClickCategoryButton = (evt: React.MouseEvent) => { const categoryID = evt.currentTarget.getAttribute("data-category-id")! document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView() - }, []) + } return
@@ -155,7 +151,7 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe })} {allowFreeform && query && }
diff --git a/web/src/ui/emojipicker/GIFPicker.tsx b/web/src/ui/emojipicker/GIFPicker.tsx index 87181d2..f7ef45e 100644 --- a/web/src/ui/emojipicker/GIFPicker.tsx +++ b/web/src/ui/emojipicker/GIFPicker.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { CSSProperties, use, useCallback, useEffect, useState } from "react" +import React, { CSSProperties, use, useEffect, useState } from "react" import { RoomStateStore, usePreference } from "@/api/statestore" import { MediaMessageEventContent } from "@/api/types" import { isMobileDevice } from "@/util/ismobile.ts" @@ -36,13 +36,11 @@ const GIFPicker = ({ style, onSelect, room }: MediaPickerProps) => { const [results, setResults] = useState([]) const [error, setError] = useState() const close = use(ModalCloseContext) - const clearQuery = useCallback(() => setQuery(""), []) - const onChangeQuery = useCallback((evt: React.ChangeEvent) => setQuery(evt.target.value), []) const client = use(ClientContext)! const provider = usePreference(client.store, room, "gif_provider") const providerName = provider.slice(0, 1).toUpperCase() + provider.slice(1) // const reuploadGIFs = room.preferences.reupload_gifs - const onSelectGIF = useCallback((evt: React.MouseEvent) => { + const onSelectGIF = (evt: React.MouseEvent) => { const idx = evt.currentTarget.getAttribute("data-gif-index") if (!idx) { return @@ -64,7 +62,7 @@ const GIFPicker = ({ style, onSelect, room }: MediaPickerProps) => { url: gif.proxied_mxc, }) close() - }, [onSelect, close, results]) + } useEffect(() => { if (!query) { if (trendingCache.has(provider)) { @@ -106,12 +104,12 @@ const GIFPicker = ({ style, onSelect, room }: MediaPickerProps) => {
setQuery(evt.target.value)} value={query} type="search" placeholder={`Search ${providerName}`} /> -
diff --git a/web/src/ui/emojipicker/StickerPicker.tsx b/web/src/ui/emojipicker/StickerPicker.tsx index dbb6488..2a3922c 100644 --- a/web/src/ui/emojipicker/StickerPicker.tsx +++ b/web/src/ui/emojipicker/StickerPicker.tsx @@ -39,7 +39,6 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => { customEmojiPacks, stickers: true, }) - const clearQuery = useCallback(() => setQuery(""), []) const close = use(ModalCloseContext) const onSelectWrapped = useCallback((emoji?: Emoji) => { if (!emoji) { @@ -53,11 +52,10 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => { }) close() }, [onSelect, close]) - const onChangeQuery = useCallback((evt: React.ChangeEvent) => setQuery(evt.target.value), []) - const onClickCategoryButton = useCallback((evt: React.MouseEvent) => { + const onClickCategoryButton = (evt: React.MouseEvent) => { const categoryID = evt.currentTarget.getAttribute("data-category-id")! document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView() - }, []) + } return
@@ -76,12 +74,12 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => {
setQuery(evt.target.value)} value={query} type="search" placeholder="Search stickers" /> -
diff --git a/web/src/ui/login/BeeperLogin.tsx b/web/src/ui/login/BeeperLogin.tsx index ac0bc90..95b2d4d 100644 --- a/web/src/ui/login/BeeperLogin.tsx +++ b/web/src/ui/login/BeeperLogin.tsx @@ -13,10 +13,9 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { useCallback, useState } from "react" +import React, { useState } from "react" import * as beeper from "@/api/beeper.ts" import type Client from "@/api/client.ts" -import useEvent from "@/util/useEvent.ts" interface BeeperLoginProps { domain: string @@ -29,18 +28,18 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => { const [code, setCode] = useState("") const [error, setError] = useState("") - const onChangeEmail = useCallback((evt: React.ChangeEvent) => { + const onChangeEmail = (evt: React.ChangeEvent) => { setEmail(evt.target.value) - }, []) - const onChangeCode = useCallback((evt: React.ChangeEvent) => { + } + const onChangeCode = (evt: React.ChangeEvent) => { let codeDigits = evt.target.value.replace(/\D/g, "").slice(0, 6) if (codeDigits.length > 3) { codeDigits = codeDigits.slice(0, 3) + " " + codeDigits.slice(3) } setCode(codeDigits) - }, []) + } - const requestCode = useEvent((evt: React.FormEvent) => { + const requestCode = (evt: React.FormEvent) => { evt.preventDefault() beeper.doStartLogin(domain).then( request => beeper.doRequestCode(domain, request, email).then( @@ -49,8 +48,8 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => { ), err => setError(`Failed to start login: ${err}`), ) - }) - const submitCode = useEvent((evt: React.FormEvent) => { + } + const submitCode = (evt: React.FormEvent) => { evt.preventDefault() beeper.doSubmitCode(domain, requestID, code).then( token => { @@ -61,7 +60,7 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => { }, err => setError(`Failed to submit code: ${err}`), ) - }) + } return

Beeper email login

diff --git a/web/src/ui/login/LoginScreen.tsx b/web/src/ui/login/LoginScreen.tsx index 58a90f5..3de5a55 100644 --- a/web/src/ui/login/LoginScreen.tsx +++ b/web/src/ui/login/LoginScreen.tsx @@ -16,7 +16,6 @@ import React, { useCallback, useEffect, useState } from "react" import type Client from "@/api/client.ts" import type { ClientState } from "@/api/types" -import useEvent from "@/util/useEvent.ts" import BeeperLogin from "./BeeperLogin.tsx" import "./LoginScreen.css" @@ -34,7 +33,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { const [loginFlows, setLoginFlows] = useState(null) const [error, setError] = useState("") - const loginSSO = useEvent(() => { + const loginSSO = () => { fetch("_gomuks/sso", { method: "POST", body: JSON.stringify({ homeserver_url: homeserverURL }), @@ -53,9 +52,9 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { }, err => setError(`Failed to start SSO login: ${err}`), ) - }) + } - const login = useEvent((evt: React.FormEvent) => { + const login = (evt: React.FormEvent) => { evt.preventDefault() if (!loginFlows) { // do nothing @@ -67,7 +66,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { err => setError(err.toString()), ) } - }) + } const resolveLoginFlows = useCallback((serverURL: string) => { client.rpc.getLoginFlows(serverURL).then( @@ -108,16 +107,10 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { clearTimeout(timeout) } }, [homeserverURL, loginFlows, resolveLoginFlows]) - const onChangeUsername = useCallback((evt: React.ChangeEvent) => { - setUsername(evt.target.value) - }, []) - const onChangePassword = useCallback((evt: React.ChangeEvent) => { - setPassword(evt.target.value) - }, []) - const onChangeHomeserverURL = useCallback((evt: React.ChangeEvent) => { + const onChangeHomeserverURL = (evt: React.ChangeEvent) => { setLoginFlows(null) setHomeserverURL(evt.target.value) - }, []) + } const supportsSSO = loginFlows?.includes("m.login.sso") ?? false const supportsPassword = loginFlows?.includes("m.login.password") @@ -130,7 +123,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { id="mxlogin-username" placeholder="User ID" value={username} - onChange={onChangeUsername} + onChange={evt => setUsername(evt.target.value)} /> { id="mxlogin-password" placeholder="Password" value={password} - onChange={onChangePassword} + onChange={evt => setPassword(evt.target.value)} />}
{supportsSSO && : null}
diff --git a/web/src/ui/rightpanel/UserInfoDeviceList.tsx b/web/src/ui/rightpanel/UserInfoDeviceList.tsx index 0c668fc..6b60456 100644 --- a/web/src/ui/rightpanel/UserInfoDeviceList.tsx +++ b/web/src/ui/rightpanel/UserInfoDeviceList.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { useCallback, useEffect, useState, useTransition } from "react" +import { useEffect, useState, useTransition } from "react" import { ScaleLoader } from "react-spinners" import Client from "@/api/client.ts" import { RoomStateStore } from "@/api/statestore" @@ -34,17 +34,19 @@ const DeviceList = ({ client, room, userID }: DeviceListProps) => { const [view, setEncryptionInfo] = useState(null) const [errors, setErrors] = useState(null) const [trackChangePending, startTransition] = useTransition() - const doTrackDeviceList = useCallback(() => { + const doTrackDeviceList = () => { startTransition(async () => { try { const resp = await client.rpc.trackUserDevices(userID) - setEncryptionInfo(resp) - setErrors(resp.errors) + startTransition(() => { + setEncryptionInfo(resp) + setErrors(resp.errors) + }) } catch (err) { - setErrors([`${err}`]) + startTransition(() => setErrors([`${err}`])) } }) - }, [client, userID]) + } useEffect(() => { setEncryptionInfo(null) setErrors(null) diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 075a1d9..93eb074 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { use, useCallback, useRef, useState } from "react" +import React, { use, useRef, useState } from "react" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import reverseMap from "@/util/reversemap.ts" @@ -38,18 +38,18 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { const [roomFilter, setRoomFilter] = useState("") const [realRoomFilter, setRealRoomFilter] = useState("") - const updateRoomFilter = useCallback((evt: React.ChangeEvent) => { + const updateRoomFilter = (evt: React.ChangeEvent) => { setRoomFilter(evt.target.value) client.store.currentRoomListFilter = toSearchableString(evt.target.value) setRealRoomFilter(client.store.currentRoomListFilter) - }, [client]) - const clearQuery = useCallback(() => { + } + const clearQuery = () => { setRoomFilter("") client.store.currentRoomListFilter = "" setRealRoomFilter("") roomFilterRef.current?.focus() - }, [client]) - const onKeyDown = useCallback((evt: React.KeyboardEvent) => { + } + const onKeyDown = (evt: React.KeyboardEvent) => { const key = keyToString(evt) if (key === "Escape") { clearQuery() @@ -62,7 +62,7 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { evt.stopPropagation() evt.preventDefault() } - }, [mainScreen, client.store, clearQuery]) + } return
diff --git a/web/src/ui/roomview/RoomPreview.tsx b/web/src/ui/roomview/RoomPreview.tsx index 8718682..d72bd6f 100644 --- a/web/src/ui/roomview/RoomPreview.tsx +++ b/web/src/ui/roomview/RoomPreview.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { use, useCallback, useEffect, useState } from "react" +import { use, useEffect, useState } from "react" import { ScaleLoader } from "react-spinners" import { getAvatarURL, getRoomAvatarURL } from "@/api/media.ts" import { InvitedRoomStore } from "@/api/statestore/invitedroom.ts" @@ -41,7 +41,7 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => { const [loading, setLoading] = useState(false) const [buttonClicked, setButtonClicked] = useState(false) const [error, setError] = useState(null) - const doJoinRoom = useCallback(() => { + const doJoinRoom = () => { let realVia = via if (!via?.length && invite?.invited_by) { realVia = [getServerName(invite.invited_by)] @@ -54,8 +54,8 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => { setButtonClicked(false) }, ) - }, [client, roomID, via, alias, invite]) - const doRejectInvite = useCallback(() => { + } + const doRejectInvite = () => { setButtonClicked(true) client.rpc.leaveRoom(roomID).then( () => { @@ -67,7 +67,7 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => { setButtonClicked(false) }, ) - }, [client, mainScreen, roomID]) + } useEffect(() => { setSummary(null) setError(null) diff --git a/web/src/ui/roomview/RoomViewHeader.tsx b/web/src/ui/roomview/RoomViewHeader.tsx index 8dd9ea2..4aa0f1d 100644 --- a/web/src/ui/roomview/RoomViewHeader.tsx +++ b/web/src/ui/roomview/RoomViewHeader.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { use, useCallback } from "react" +import { use } from "react" import { getRoomAvatarURL } from "@/api/media.ts" import { RoomStateStore } from "@/api/statestore" import { useEventAsState } from "@/util/eventdispatcher.ts" @@ -35,14 +35,14 @@ const RoomViewHeader = ({ room }: RoomViewHeaderProps) => { const roomMeta = useEventAsState(room.meta) const mainScreen = use(MainScreenContext) const openModal = use(ModalContext) - const openSettings = useCallback(() => { + const openSettings = () => { openModal({ dimmed: true, boxed: true, innerBoxClass: "settings-view", content: , }) - }, [room, openModal]) + } return
{ inheritedValue: T } -const useRemover = ( +const makeRemover = ( context: PreferenceContext, setPref: SetPrefFunc, name: keyof Preferences, value: PreferenceValueType | undefined, ) => { - const onClear = useCallback(() => { - setPref(context, name, undefined) - }, [setPref, context, name]) if (value === undefined) { return null } - return + return } const BooleanPreferenceCell = ({ context, name, setPref, value, inheritedValue }: PreferenceCellProps) => { - const onChange = useCallback((evt: React.ChangeEvent) => { - setPref(context, name, evt.target.checked) - }, [setPref, context, name]) return
- - {useRemover(context, setPref, name, value)} + setPref(context, name, evt.target.checked)}/> + {makeRemover(context, setPref, name, value)}
} const TextPreferenceCell = ({ context, name, setPref, value, inheritedValue }: PreferenceCellProps) => { - const onChange = useCallback((evt: React.ChangeEvent) => { - setPref(context, name, evt.target.value) - }, [setPref, context, name]) return
- - {useRemover(context, setPref, name, value)} + setPref(context, name, evt.target.value)}/> + {makeRemover(context, setPref, name, value)}
} const SelectPreferenceCell = ({ context, name, pref, setPref, value, inheritedValue }: PreferenceCellProps) => { - const onChange = useCallback((evt: React.ChangeEvent) => { - setPref(context, name, evt.target.value) - }, [setPref, context, name]) - const remover = useRemover(context, setPref, name, value) if (!pref.allowedValues) { return null } return
- setPref(context, name, evt.target.value)}> {pref.allowedValues.map(value => )} - {remover} + {makeRemover(context, setPref, name, value)}
} @@ -186,7 +173,7 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta const client = use(ClientContext)! const appliedContext = getActiveCSSContext(client, room) const [context, setContext] = useState(appliedContext) - const getContextText = useCallback((context: PreferenceContext) => { + const getContextText = (context: PreferenceContext) => { if (context === PreferenceContext.Account) { return client.store.serverPreferenceCache.custom_css } else if (context === PreferenceContext.Device) { @@ -196,17 +183,17 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta } else if (context === PreferenceContext.RoomDevice) { return room.localPreferenceCache.custom_css } - }, [client, room]) + } const origText = getContextText(context) const [text, setText] = useState(origText ?? "") - const onChangeContext = useCallback((evt: React.ChangeEvent) => { + const onChangeContext = (evt: React.ChangeEvent) => { const newContext = evt.target.value as PreferenceContext setContext(newContext) setText(getContextText(newContext) ?? "") - }, [getContextText]) - const onChangeText = useCallback((evt: React.ChangeEvent) => { + } + const onChangeText = (evt: React.ChangeEvent) => { setText(evt.target.value) - }, []) + } const onSave = useEvent(() => { if (vscodeOpen) { setText(vscodeContentRef.current) @@ -215,18 +202,18 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta setPref(context, "custom_css", text) } }) - const onDelete = useEvent(() => { + const onDelete = () => { setPref(context, "custom_css", undefined) setText("") - }) + } const [vscodeOpen, setVSCodeOpen] = useState(false) const vscodeContentRef = useRef("") const vscodeInitialContentRef = useRef("") - const onClickVSCode = useEvent(() => { + const onClickVSCode = () => { vscodeContentRef.current = text vscodeInitialContentRef.current = text setVSCodeOpen(true) - }) + } const closeVSCode = useCallback(() => { setVSCodeOpen(false) setText(vscodeContentRef.current) @@ -296,7 +283,9 @@ const SettingsView = ({ room }: SettingsViewProps) => { const roomMeta = useEventAsState(room.meta) const client = use(ClientContext)! const closeModal = use(ModalCloseContext) - const setPref = useCallback((context: PreferenceContext, key: keyof Preferences, value: PreferenceValueType | undefined) => { + const setPref = useCallback(( + context: PreferenceContext, key: keyof Preferences, value: PreferenceValueType | undefined, + ) => { if (context === PreferenceContext.Account) { client.rpc.setAccountData("fi.mau.gomuks.preferences", { ...client.store.serverPreferenceCache, @@ -321,15 +310,15 @@ const SettingsView = ({ room }: SettingsViewProps) => { } } }, [client, room]) - const onClickLogout = useCallback(() => { + const onClickLogout = () => { if (window.confirm("Really log out and delete all local data?")) { client.logout().then( () => console.info("Successfully logged out"), err => window.alert(`Failed to log out: ${err}`), ) } - }, [client]) - const onClickLeave = useCallback(() => { + } + const onClickLeave = () => { if (window.confirm(`Really leave ${room.meta.current.name}?`)) { client.rpc.leaveRoom(room.roomID).then( () => { @@ -339,7 +328,7 @@ const SettingsView = ({ room }: SettingsViewProps) => { err => window.alert(`Failed to leave room: ${err}`), ) } - }, [client, room, closeModal]) + } usePreferences(client.store, room) const globalServer = client.store.serverPreferenceCache const globalLocal = client.store.localPreferenceCache diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index e01dbb8..7e34e18 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { JSX, use, useCallback, useState } from "react" +import React, { JSX, use, useState } from "react" import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts" import { useRoomMember } from "@/api/statestore" import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types" @@ -79,7 +79,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven const mainScreen = use(MainScreenContext) const openModal = use(ModalContext) const [forceContextMenuOpen, setForceContextMenuOpen] = useState(false) - const onContextMenu = useCallback((mouseEvt: React.MouseEvent) => { + const onContextMenu = (mouseEvt: React.MouseEvent) => { const targetElem = mouseEvt.target as HTMLElement if ( !roomCtx.store.preferences.message_context_menu @@ -97,7 +97,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven style={getModalStyleFromMouse(mouseEvt, 9 * 40)} />, }) - }, [openModal, evt, roomCtx]) + } const memberEvt = useRoomMember(client, roomCtx.store, evt.sender) const memberEvtContent = memberEvt?.content as MemberEventContent | undefined const BodyType = getBodyType(evt) diff --git a/web/src/ui/timeline/TimelineView.tsx b/web/src/ui/timeline/TimelineView.tsx index b2c01ec..9bb8903 100644 --- a/web/src/ui/timeline/TimelineView.tsx +++ b/web/src/ui/timeline/TimelineView.tsx @@ -46,13 +46,13 @@ const TimelineView = () => { // When the user scrolls the timeline manually, remember if they were at the bottom, // so that we can keep them at the bottom when new events are added. - const handleScroll = useCallback(() => { + const handleScroll = () => { if (!timelineViewRef.current) { return } const timelineView = timelineViewRef.current roomCtx.scrolledToBottom = timelineView.scrollTop + timelineView.clientHeight + 1 >= timelineView.scrollHeight - }, [roomCtx]) + } // Save the scroll height prior to updating the timeline, so that we can adjust the scroll position if needed. if (timelineViewRef.current) { oldScrollHeight.current = timelineViewRef.current.scrollHeight diff --git a/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx b/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx index f59b2f5..ff3ed8b 100644 --- a/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx +++ b/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx @@ -13,9 +13,9 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { use, useCallback, useState } from "react" +import React, { use, useState } from "react" import { MemDBEvent } from "@/api/types" -import useEvent from "@/util/useEvent.ts" +import { isMobileDevice } from "@/util/ismobile.ts" import { ModalCloseContext } from "../../modal/Modal.tsx" import TimelineEvent from "../TimelineEvent.tsx" @@ -33,14 +33,11 @@ const ConfirmWithMessageModal = ({ }: ConfirmWithMessageProps) => { const [reason, setReason] = useState("") const closeModal = use(ModalCloseContext) - const onConfirmWrapped = useEvent((evt: React.FormEvent) => { + const onConfirmWrapped = (evt: React.FormEvent) => { evt.preventDefault() closeModal() onConfirm(reason) - }) - const onChangeReason = useCallback((evt: React.ChangeEvent) => { - setReason(evt.target.value) - }, []) + } return

{title}

@@ -49,7 +46,13 @@ const ConfirmWithMessageModal = ({
{description}
- + setReason(evt.target.value)} + />
diff --git a/web/src/ui/timeline/menu/usePrimaryItems.tsx b/web/src/ui/timeline/menu/usePrimaryItems.tsx index b53a64a..f570f40 100644 --- a/web/src/ui/timeline/menu/usePrimaryItems.tsx +++ b/web/src/ui/timeline/menu/usePrimaryItems.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { CSSProperties, use, useCallback } from "react" +import React, { CSSProperties, use } from "react" import Client from "@/api/client.ts" import { MemDBEvent } from "@/api/types" import { emojiToReactionContent } from "@/util/emoji" @@ -43,11 +43,11 @@ export const usePrimaryItems = ( const closeModal = !isHover ? use(ModalCloseContext) : noop const openModal = use(ModalContext) - const onClickReply = useCallback(() => { + const onClickReply = () => { roomCtx.setReplyTo(evt.event_id) closeModal() - }, [roomCtx, evt.event_id, closeModal]) - const onClickReact = useCallback((mevt: React.MouseEvent) => { + } + const onClickReact = (mevt: React.MouseEvent) => { const emojiPickerHeight = 34 * 16 setForceOpen?.(true) openModal({ @@ -63,20 +63,20 @@ export const usePrimaryItems = ( />, onClose: () => setForceOpen?.(false), }) - }, [client, roomCtx, evt, style, setForceOpen, openModal]) - const onClickEdit = useCallback(() => { + } + const onClickEdit = () => { closeModal() roomCtx.setEditing(evt) - }, [roomCtx, evt, closeModal]) - const onClickResend = useCallback(() => { + } + const onClickResend = () => { if (!evt.transaction_id) { return } closeModal() client.resendEvent(evt.transaction_id) .catch(err => window.alert(`Failed to resend message: ${err}`)) - }, [client, evt.transaction_id, closeModal]) - const onClickMore = useCallback((mevt: React.MouseEvent) => { + } + const onClickMore = (mevt: React.MouseEvent) => { const moreMenuHeight = 4 * 40 setForceOpen!(true) openModal({ @@ -87,7 +87,7 @@ export const usePrimaryItems = ( />, onClose: () => setForceOpen!(false), }) - }, [evt, roomCtx, setForceOpen, openModal]) + } const isEditing = useEventAsState(roomCtx.isEditing) const [isPending, pendingTitle] = getPending(evt) const isEncrypted = getEncryption(roomCtx.store) diff --git a/web/src/ui/timeline/menu/useSecondaryItems.tsx b/web/src/ui/timeline/menu/useSecondaryItems.tsx index 1aca07d..046aff4 100644 --- a/web/src/ui/timeline/menu/useSecondaryItems.tsx +++ b/web/src/ui/timeline/menu/useSecondaryItems.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { use, useCallback } from "react" +import { use } from "react" import Client from "@/api/client.ts" import { useRoomState } from "@/api/statestore" import { MemDBEvent } from "@/api/types" @@ -35,14 +35,14 @@ export const useSecondaryItems = ( ) => { const closeModal = use(ModalCloseContext) const openModal = use(ModalContext) - const onClickViewSource = useCallback(() => { + const onClickViewSource = () => { openModal({ dimmed: true, boxed: true, content: , }) - }, [evt, openModal]) - const onClickReport = useCallback(() => { + } + const onClickReport = () => { openModal({ dimmed: true, boxed: true, @@ -61,8 +61,8 @@ export const useSecondaryItems = ( /> , }) - }, [evt, roomCtx, openModal, client]) - const onClickRedact = useCallback(() => { + } + const onClickRedact = () => { openModal({ dimmed: true, boxed: true, @@ -81,17 +81,12 @@ export const useSecondaryItems = ( /> , }) - }, [evt, roomCtx, openModal, client]) - const onClickPin = useCallback(() => { + } + const onClickPin = (pin: boolean) => () => { closeModal() - client.pinMessage(roomCtx.store, evt.event_id, true) - .catch(err => window.alert(`Failed to pin message: ${err}`)) - }, [closeModal, client, roomCtx, evt.event_id]) - const onClickUnpin = useCallback(() => { - closeModal() - client.pinMessage(roomCtx.store, evt.event_id, false) - .catch(err => window.alert(`Failed to unpin message: ${err}`)) - }, [closeModal, client, roomCtx, evt.event_id]) + client.pinMessage(roomCtx.store, evt.event_id, pin) + .catch(err => window.alert(`Failed to ${pin ? "pin" : "unpin"} message: ${err}`)) + } const [isPending, pendingTitle] = getPending(evt) useRoomState(roomCtx.store, "m.room.power_levels", "") @@ -109,8 +104,12 @@ export const useSecondaryItems = ( return <> {ownPL >= pinPL && (pins.includes(evt.event_id) - ? - : )} + ? + : )} {canRedact &&