1
0
Fork 0
forked from Mirrors/gomuks

web/all: remove unnecessary uses of useCallback

This commit is contained in:
Tulir Asokan 2024-12-22 14:54:57 +02:00
parent 388be09795
commit 4160a33edb
23 changed files with 285 additions and 290 deletions

View file

@ -80,13 +80,13 @@ function useAutocompleter<T>({
})
document.querySelector(`div.autocompletion-item[data-index='${index}']`)?.scrollIntoView({ block: "nearest" })
})
const onClick = useEvent((evt: React.MouseEvent<HTMLDivElement>) => {
const onClick = (evt: React.MouseEvent<HTMLDivElement>) => {
const idx = evt.currentTarget.getAttribute("data-index")
if (idx) {
onSelect(+idx)
setAutocomplete(null)
}
})
}
useEffect(() => {
if (params.selected !== undefined) {
onSelect(params.selected)

View file

@ -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 <https://www.gnu.org/licenses/>.
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 <div className="composer-media">
<div className={`media-container ${containerClass}`} style={containerStyle}>
{mediaContent}
</div>
{clearMedia && <button onClick={clearMedia}><CloseIcon/></button>}
</div>
}
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 <div className="composer-location">
<div className="location-container">
<LeafletPicker tileTemplate={tileTemplate} onChange={onChange} initialLocation={location}/>
</div>
<button onClick={clearLocation}><CloseIcon/></button>
</div>
}

View file

@ -15,8 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<HTMLTextAreaElement>, newText?: string) => {
const onComposerCaretChange = (evt: CaretEvent<HTMLTextAreaElement>, 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<HTMLTextAreaElement>) => {
}
const onComposerKeyDown = (evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
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<AutocompleteQuery> | null | undefined
if (fullKey === "Tab" || fullKey === "ArrowDown") {
@ -340,8 +330,8 @@ const MessageComposer = () => {
evt.stopPropagation()
roomCtx.setEditing(null)
}
})
const onChange = useEvent((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
}
const onChange = (evt: React.ChangeEvent<HTMLTextAreaElement>) => {
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<HTMLInputElement>) => doUploadFile(evt.target.files?.[0]),
)
const onPaste = useEvent((evt: React.ClipboardEvent<HTMLTextAreaElement>) => {
const onPaste = (evt: React.ClipboardEvent<HTMLTextAreaElement>) => {
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: <EmojiPicker
style={getEmojiPickerStyle()}
room={roomCtx.store}
onSelect={(emoji: PartialEmoji) => {
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: <GIFPicker
style={getEmojiPickerStyle()}
room={roomCtx.store}
onSelect={media => setState({ media })}
/>,
onClose: () => !isMobileDevice && textInput.current?.focus(),
})
})
const openStickerPicker = useEvent(() => {
openModal({
content: <StickerPicker
style={getEmojiPickerStyle()}
room={roomCtx.store}
onSelect={media => 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: <EmojiPicker
style={getEmojiPickerStyle()}
room={roomCtx.store}
onSelect={(emoji: PartialEmoji) => {
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: <GIFPicker
style={getEmojiPickerStyle()}
room={roomCtx.store}
onSelect={media => setState({ media })}
/>,
onClose: () => !isMobileDevice && textInput.current?.focus(),
})
}
const openStickerPicker = () => {
openModal({
content: <StickerPicker
style={getEmojiPickerStyle()}
room={roomCtx.store}
onSelect={media => doSendMessage({ ...state, media, text: "" })}
/>,
onClose: () => !isMobileDevice && textInput.current?.focus(),
})
}
const openLocationPicker = () => {
setState({ location: { lat: 0, long: 0, prec: 1 }, media: null })
}
return <>
<button onClick={openEmojiPicker} title="Add emoji"><EmojiIcon/>{includeText && "Emoji"}</button>
<button
@ -556,13 +542,13 @@ const MessageComposer = () => {
title={locationDisabledTitle ?? "Add location"}
><LocationIcon/>{includeText && "Location"}</button>
<button
onClick={openFilePicker}
onClick={() => fileInput.current!.click()}
disabled={!!mediaDisabledTitle}
title={mediaDisabledTitle ?? "Add file attachment"}
><AttachIcon/>{includeText && "File"}</button>
</>
}
const openButtonsModal = useEvent(() => {
const openButtonsModal = () => {
const style: CSSProperties = getEmojiPickerStyle()
style.left = style.right
delete style.right
@ -571,7 +557,7 @@ const MessageComposer = () => {
{makeAttachmentButtons(true)}
</div>,
})
})
}
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 && <button
onClick={sendMessage}
onClick={onClickSend}
disabled={!canSend || loadingMedia}
title="Send message"
><SendIcon/></button>}
<input ref={fileInput} onChange={onAttachFile} type="file" value=""/>
<input
ref={fileInput}
onChange={evt => doUploadFile(evt.target.files?.[0])}
type="file"
value=""
/>
</div>
</div>
</>
}
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 <div className="composer-media">
<div className={`media-container ${containerClass}`} style={containerStyle}>
{mediaContent}
</div>
{clearMedia && <button onClick={clearMedia}><CloseIcon/></button>}
</div>
}
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 <div className="composer-location">
<div className="location-container">
<LeafletPicker tileTemplate={tileTemplate} onChange={onChange} initialLocation={location}/>
</div>
<button onClick={clearLocation}><CloseIcon/></button>
</div>
}
export default MessageComposer

View file

@ -13,11 +13,10 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<HTMLButtonElement>) =>
onSelect(getEmojiFromAttrs(evt.currentTarget)))
const onMouseOverEmoji = useEvent((evt: React.MouseEvent<HTMLButtonElement>) =>
setPreviewEmoji?.(getEmojiFromAttrs(evt.currentTarget)))
const onMouseOutEmoji = useCallback(() => setPreviewEmoji?.(undefined), [setPreviewEmoji])
const onClickSubscribePack = useEvent((evt: React.MouseEvent<HTMLButtonElement>) => {
const onMouseOverEmoji = setPreviewEmoji && ((evt: React.MouseEvent<HTMLButtonElement>) =>
setPreviewEmoji(getEmojiFromAttrs(evt.currentTarget)))
const onMouseOutEmoji = setPreviewEmoji && (() => setPreviewEmoji(undefined))
const onClickSubscribePack = (evt: React.MouseEvent<HTMLButtonElement>) => {
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<HTMLButtonElement>) => {
}
const onClickUnsubscribePack = (evt: React.MouseEvent<HTMLButtonElement>) => {
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)}</button>) : null}
</div>

View file

@ -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<HTMLInputElement>) => 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 <div className="emoji-picker" style={style}>
<div className="emoji-category-bar" ref={emojiCategoryBarRef}>
<button
@ -123,12 +119,12 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe
<div className="emoji-search">
<input
autoFocus={!isMobileDevice}
onChange={onChangeQuery}
onChange={evt => setQuery(evt.target.value)}
value={query}
type="search"
placeholder="Search emojis"
/>
<button onClick={clearQuery} disabled={query === ""}>
<button onClick={() => setQuery("")} disabled={query === ""}>
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
</button>
</div>
@ -155,7 +151,7 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe
})}
{allowFreeform && query && <button
className="freeform-react"
onClick={onClickFreeformReact}
onClick={() => onSelectWrapped({ u: query })}
>React with "{query}"</button>}
</div>
</div>

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<GIF[]>([])
const [error, setError] = useState<unknown>()
const close = use(ModalCloseContext)
const clearQuery = useCallback(() => setQuery(""), [])
const onChangeQuery = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => 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<HTMLDivElement>) => {
const onSelectGIF = (evt: React.MouseEvent<HTMLDivElement>) => {
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) => {
<div className="gif-search">
<input
autoFocus={!isMobileDevice}
onChange={onChangeQuery}
onChange={evt => setQuery(evt.target.value)}
value={query}
type="search"
placeholder={`Search ${providerName}`}
/>
<button onClick={clearQuery} disabled={query === ""}>
<button onClick={() => setQuery("")} disabled={query === ""}>
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
</button>
</div>

View file

@ -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<HTMLInputElement>) => 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 <div className="sticker-picker" style={style}>
<div className="emoji-category-bar" ref={emojiCategoryBarRef}>
@ -76,12 +74,12 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => {
<div className="emoji-search">
<input
autoFocus={!isMobileDevice}
onChange={onChangeQuery}
onChange={evt => setQuery(evt.target.value)}
value={query}
type="search"
placeholder="Search stickers"
/>
<button onClick={clearQuery} disabled={query === ""}>
<button onClick={() => setQuery("")} disabled={query === ""}>
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
</button>
</div>

View file

@ -13,10 +13,9 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<HTMLInputElement>) => {
const onChangeEmail = (evt: React.ChangeEvent<HTMLInputElement>) => {
setEmail(evt.target.value)
}, [])
const onChangeCode = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
}
const onChangeCode = (evt: React.ChangeEvent<HTMLInputElement>) => {
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 <form onSubmit={requestID ? submitCode : requestCode} className="beeper-login">
<h2>Beeper email login</h2>

View file

@ -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<string[] | null>(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<HTMLInputElement>) => {
setUsername(evt.target.value)
}, [])
const onChangePassword = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
setPassword(evt.target.value)
}, [])
const onChangeHomeserverURL = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
const onChangeHomeserverURL = (evt: React.ChangeEvent<HTMLInputElement>) => {
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)}
/>
<input
type="text"
@ -144,7 +137,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => {
id="mxlogin-password"
placeholder="Password"
value={password}
onChange={onChangePassword}
onChange={evt => setPassword(evt.target.value)}
/>}
<div className="buttons">
{supportsSSO && <button

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { useCallback, useState } from "react"
import React, { useState } from "react"
import { LoginScreenProps } from "./LoginScreen.tsx"
import "./LoginScreen.css"
@ -24,13 +24,13 @@ export const VerificationScreen = ({ client, clientState }: LoginScreenProps) =>
const [recoveryKey, setRecoveryKey] = useState("")
const [error, setError] = useState("")
const verify = useCallback((evt: React.FormEvent) => {
const verify = (evt: React.FormEvent) => {
evt.preventDefault()
client.rpc.verify(recoveryKey).then(
() => {},
err => setError(err.toString()),
)
}, [recoveryKey, client])
}
return <main className="matrix-login">
<h1>gomuks web</h1>

View file

@ -45,7 +45,7 @@ export const ModalWrapper = ({ children }: { children: React.ReactNode }) => {
history.back()
}
}, [])
const onKeyWrapper = useCallback((evt: React.KeyboardEvent<HTMLDivElement>) => {
const onKeyWrapper = (evt: React.KeyboardEvent<HTMLDivElement>) => {
if (evt.key === "Escape") {
setState(null)
if (history.state?.modal) {
@ -53,7 +53,7 @@ export const ModalWrapper = ({ children }: { children: React.ReactNode }) => {
}
}
evt.stopPropagation()
}, [])
}
const openModal = useCallback((newState: ModalState) => {
if (!history.state?.modal) {
history.pushState({ ...(history.state ?? {}), modal: true }, "")

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { use, useCallback, useState } from "react"
import React, { use, useState } from "react"
import { getAvatarURL } from "@/api/media.ts"
import { MemDBEvent, MemberEventContent } from "@/api/types"
import { getDisplayname } from "@/util/validation.ts"
@ -45,8 +45,6 @@ const MemberRow = ({ evt, onClick }: MemberRowProps) => {
const MemberList = () => {
const [filter, setFilter] = useState("")
const [limit, setLimit] = useState(30)
const increaseLimit = useCallback(() => setLimit(limit => limit + 50), [])
const onChangeFilter = useCallback((e: React.ChangeEvent<HTMLInputElement>) => setFilter(e.target.value), [])
const roomCtx = use(RoomContext)
if (roomCtx?.store && !roomCtx?.store.membersRequested && !roomCtx?.store.fullMembersLoaded) {
roomCtx.store.membersRequested = true
@ -69,10 +67,15 @@ const MemberList = () => {
}
}
return <>
<input className="member-filter" value={filter} onChange={onChangeFilter} placeholder="Filter members" />
<input
className="member-filter"
value={filter}
onChange={evt => setFilter(evt.target.value)}
placeholder="Filter members"
/>
<div className="member-list">
{members}
{memberEvents.length > limit ? <button onClick={increaseLimit}>
{memberEvents.length > limit ? <button onClick={() => setLimit(limit => limit + 50)}>
and {memberEvents.length - limit} others
</button> : null}
</div>

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<ProfileEncryptionInfo | null>(null)
const [errors, setErrors] = useState<string[] | null>(null)
const [trackChangePending, startTransition] = useTransition()
const doTrackDeviceList = useCallback(() => {
const doTrackDeviceList = () => {
startTransition(async () => {
try {
const resp = await client.rpc.trackUserDevices(userID)
startTransition(() => {
setEncryptionInfo(resp)
setErrors(resp.errors)
})
} catch (err) {
setErrors([`${err}`])
startTransition(() => setErrors([`${err}`]))
}
})
}, [client, userID])
}
useEffect(() => {
setEncryptionInfo(null)
setErrors(null)

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<HTMLInputElement>) => {
const updateRoomFilter = (evt: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
}
const onKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => {
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 <div className="room-list-wrapper">
<div className="room-search-wrapper">

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<string | null>(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)

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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: <SettingsView room={room} />,
})
}, [room, openModal])
}
return <div className="room-header">
<button className="back" onClick={mainScreen.clearActiveRoom}><BackIcon/></button>
<img

View file

@ -45,52 +45,39 @@ interface PreferenceCellProps<T extends PreferenceValueType> {
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 <button onClick={onClear}><CloseIcon /></button>
return <button onClick={() => setPref(context, name, undefined)}><CloseIcon /></button>
}
const BooleanPreferenceCell = ({ context, name, setPref, value, inheritedValue }: PreferenceCellProps<boolean>) => {
const onChange = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
setPref(context, name, evt.target.checked)
}, [setPref, context, name])
return <div className="preference boolean-preference">
<Toggle checked={value ?? inheritedValue} onChange={onChange}/>
{useRemover(context, setPref, name, value)}
<Toggle checked={value ?? inheritedValue} onChange={evt => setPref(context, name, evt.target.checked)}/>
{makeRemover(context, setPref, name, value)}
</div>
}
const TextPreferenceCell = ({ context, name, setPref, value, inheritedValue }: PreferenceCellProps<string>) => {
const onChange = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
setPref(context, name, evt.target.value)
}, [setPref, context, name])
return <div className="preference string-preference">
<input value={value ?? inheritedValue} onChange={onChange}/>
{useRemover(context, setPref, name, value)}
<input value={value ?? inheritedValue} onChange={evt => setPref(context, name, evt.target.value)}/>
{makeRemover(context, setPref, name, value)}
</div>
}
const SelectPreferenceCell = ({ context, name, pref, setPref, value, inheritedValue }: PreferenceCellProps<string>) => {
const onChange = useCallback((evt: React.ChangeEvent<HTMLSelectElement>) => {
setPref(context, name, evt.target.value)
}, [setPref, context, name])
const remover = useRemover(context, setPref, name, value)
if (!pref.allowedValues) {
return null
}
return <div className="preference select-preference">
<select value={value ?? inheritedValue} onChange={onChange}>
<select value={value ?? inheritedValue} onChange={evt => setPref(context, name, evt.target.value)}>
{pref.allowedValues.map(value =>
<option key={value} value={value}>{value}</option>)}
</select>
{remover}
{makeRemover(context, setPref, name, value)}
</div>
}
@ -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<HTMLSelectElement>) => {
const onChangeContext = (evt: React.ChangeEvent<HTMLSelectElement>) => {
const newContext = evt.target.value as PreferenceContext
setContext(newContext)
setText(getContextText(newContext) ?? "")
}, [getContextText])
const onChangeText = useCallback((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
}
const onChangeText = (evt: React.ChangeEvent<HTMLTextAreaElement>) => {
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

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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)

View file

@ -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

View file

@ -13,9 +13,9 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<HTMLInputElement>) => {
setReason(evt.target.value)
}, [])
}
return <form onSubmit={onConfirmWrapped}>
<h3>{title}</h3>
<div className="timeline-event-container">
@ -49,7 +46,13 @@ const ConfirmWithMessageModal = ({
<div className="confirm-description">
{description}
</div>
<input autoFocus value={reason} type="text" placeholder={placeholder} onChange={onChangeReason} />
<input
autoFocus={!isMobileDevice}
value={reason}
type="text"
placeholder={placeholder}
onChange={evt => setReason(evt.target.value)}
/>
<div className="confirm-buttons">
<button type="button" onClick={closeModal}>Cancel</button>
<button type="submit">{confirmButton}</button>

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<HTMLButtonElement>) => {
}
const onClickReact = (mevt: React.MouseEvent<HTMLButtonElement>) => {
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<HTMLButtonElement>) => {
}
const onClickMore = (mevt: React.MouseEvent<HTMLButtonElement>) => {
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)

View file

@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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: <JSONView data={evt} />,
})
}, [evt, openModal])
const onClickReport = useCallback(() => {
}
const onClickReport = () => {
openModal({
dimmed: true,
boxed: true,
@ -61,8 +61,8 @@ export const useSecondaryItems = (
/>
</RoomContext>,
})
}, [evt, roomCtx, openModal, client])
const onClickRedact = useCallback(() => {
}
const onClickRedact = () => {
openModal({
dimmed: true,
boxed: true,
@ -81,17 +81,12 @@ export const useSecondaryItems = (
/>
</RoomContext>,
})
}, [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 <>
<button onClick={onClickViewSource}><ViewSourceIcon/>View source</button>
{ownPL >= pinPL && (pins.includes(evt.event_id)
? <button onClick={onClickUnpin}><UnpinIcon/>Unpin message</button>
: <button onClick={onClickPin} title={pendingTitle} disabled={isPending}><PinIcon/>Pin message</button>)}
? <button onClick={onClickPin(false)}>
<UnpinIcon/>Unpin message
</button>
: <button onClick={onClickPin(true)} title={pendingTitle} disabled={isPending}>
<PinIcon/>Pin message
</button>)}
<button onClick={onClickReport} disabled={isPending} title={pendingTitle}><ReportIcon/>Report</button>
{canRedact && <button
onClick={onClickRedact}

View file

@ -14,7 +14,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { CSSProperties } from "react"
import useEvent from "@/util/useEvent.ts"
import "./ResizeHandle.css"
export interface ResizeHandleProps {
@ -28,7 +27,7 @@ export interface ResizeHandleProps {
}
const ResizeHandle = ({ width, minWidth, maxWidth, setWidth, style, className, inverted }: ResizeHandleProps) => {
const onMouseDown = useEvent((evt: React.MouseEvent<HTMLDivElement>) => {
const onMouseDown = (evt: React.MouseEvent<HTMLDivElement>) => {
const origWidth = width
const startPos = evt.clientX
const onMouseMove = (evt: MouseEvent) => {
@ -46,7 +45,7 @@ const ResizeHandle = ({ width, minWidth, maxWidth, setWidth, style, className, i
document.addEventListener("mousemove", onMouseMove)
document.addEventListener("mouseup", onMouseUp)
evt.preventDefault()
})
}
return <div className={`resize-handle-outer ${className ?? ""}`} style={style}>
<div className="resize-handle-inner" onMouseDown={onMouseDown}/>
</div>