forked from Mirrors/gomuks
web/all: remove unnecessary uses of useCallback
This commit is contained in:
parent
388be09795
commit
4160a33edb
23 changed files with 285 additions and 290 deletions
|
@ -80,13 +80,13 @@ function useAutocompleter<T>({
|
||||||
})
|
})
|
||||||
document.querySelector(`div.autocompletion-item[data-index='${index}']`)?.scrollIntoView({ block: "nearest" })
|
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")
|
const idx = evt.currentTarget.getAttribute("data-index")
|
||||||
if (idx) {
|
if (idx) {
|
||||||
onSelect(+idx)
|
onSelect(+idx)
|
||||||
setAutocomplete(null)
|
setAutocomplete(null)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (params.selected !== undefined) {
|
if (params.selected !== undefined) {
|
||||||
onSelect(params.selected)
|
onSelect(params.selected)
|
||||||
|
|
63
web/src/ui/composer/ComposerMedia.tsx
Normal file
63
web/src/ui/composer/ComposerMedia.tsx
Normal 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>
|
||||||
|
}
|
|
@ -15,8 +15,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// 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 React, { CSSProperties, use, useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from "react"
|
||||||
import { ScaleLoader } from "react-spinners"
|
import { ScaleLoader } from "react-spinners"
|
||||||
import Client from "@/api/client.ts"
|
import { useRoomEvent } from "@/api/statestore"
|
||||||
import { RoomStateStore, usePreference, useRoomEvent } from "@/api/statestore"
|
|
||||||
import type {
|
import type {
|
||||||
EventID,
|
EventID,
|
||||||
MediaMessageEventContent,
|
MediaMessageEventContent,
|
||||||
|
@ -29,21 +28,18 @@ import type {
|
||||||
import { PartialEmoji, emojiToMarkdown } from "@/util/emoji"
|
import { PartialEmoji, emojiToMarkdown } from "@/util/emoji"
|
||||||
import { isMobileDevice } from "@/util/ismobile.ts"
|
import { isMobileDevice } from "@/util/ismobile.ts"
|
||||||
import { escapeMarkdown } from "@/util/markdown.ts"
|
import { escapeMarkdown } from "@/util/markdown.ts"
|
||||||
import useEvent from "@/util/useEvent.ts"
|
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import EmojiPicker from "../emojipicker/EmojiPicker.tsx"
|
import EmojiPicker from "../emojipicker/EmojiPicker.tsx"
|
||||||
import GIFPicker from "../emojipicker/GIFPicker.tsx"
|
import GIFPicker from "../emojipicker/GIFPicker.tsx"
|
||||||
import StickerPicker from "../emojipicker/StickerPicker.tsx"
|
import StickerPicker from "../emojipicker/StickerPicker.tsx"
|
||||||
import { keyToString } from "../keybindings.ts"
|
import { keyToString } from "../keybindings.ts"
|
||||||
import { LeafletPicker } from "../maps/async.tsx"
|
|
||||||
import { ModalContext } from "../modal/Modal.tsx"
|
import { ModalContext } from "../modal/Modal.tsx"
|
||||||
import { useRoomContext } from "../roomview/roomcontext.ts"
|
import { useRoomContext } from "../roomview/roomcontext.ts"
|
||||||
import { ReplyBody } from "../timeline/ReplyBody.tsx"
|
import { ReplyBody } from "../timeline/ReplyBody.tsx"
|
||||||
import { useMediaContent } from "../timeline/content/useMediaContent.tsx"
|
|
||||||
import type { AutocompleteQuery } from "./Autocompleter.tsx"
|
import type { AutocompleteQuery } from "./Autocompleter.tsx"
|
||||||
|
import { ComposerLocation, ComposerLocationValue, ComposerMedia } from "./ComposerMedia.tsx"
|
||||||
import { charToAutocompleteType, emojiQueryRegex, getAutocompleter } from "./getAutocompleter.ts"
|
import { charToAutocompleteType, emojiQueryRegex, getAutocompleter } from "./getAutocompleter.ts"
|
||||||
import AttachIcon from "@/icons/attach.svg?react"
|
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 EmojiIcon from "@/icons/emoji-categories/smileys-emotion.svg?react"
|
||||||
import GIFIcon from "@/icons/gif.svg?react"
|
import GIFIcon from "@/icons/gif.svg?react"
|
||||||
import LocationIcon from "@/icons/location.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 StickerIcon from "@/icons/sticker.svg?react"
|
||||||
import "./MessageComposer.css"
|
import "./MessageComposer.css"
|
||||||
|
|
||||||
export interface ComposerLocationValue {
|
|
||||||
lat: number
|
|
||||||
long: number
|
|
||||||
prec?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComposerState {
|
export interface ComposerState {
|
||||||
text: string
|
text: string
|
||||||
media: MediaMessageEventContent | null
|
media: MediaMessageEventContent | null
|
||||||
|
@ -174,13 +164,13 @@ const MessageComposer = () => {
|
||||||
textInput.current?.focus()
|
textInput.current?.focus()
|
||||||
}, [room.roomID])
|
}, [room.roomID])
|
||||||
const canSend = Boolean(state.text || state.media || state.location)
|
const canSend = Boolean(state.text || state.media || state.location)
|
||||||
const sendMessage = useEvent((evt: React.FormEvent) => {
|
const onClickSend = (evt: React.FormEvent) => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
if (!canSend) {
|
if (!canSend) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
doSendMessage(state)
|
doSendMessage(state)
|
||||||
})
|
}
|
||||||
const doSendMessage = (state: ComposerState) => {
|
const doSendMessage = (state: ComposerState) => {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
setState(draftStore.get(room.roomID) ?? emptyComposer)
|
setState(draftStore.get(room.roomID) ?? emptyComposer)
|
||||||
|
@ -245,7 +235,7 @@ const MessageComposer = () => {
|
||||||
mentions,
|
mentions,
|
||||||
}).catch(err => window.alert("Failed to send message: " + err))
|
}).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
|
const area = evt.currentTarget
|
||||||
if (area.selectionStart <= (autocomplete?.startPos ?? 0)) {
|
if (area.selectionStart <= (autocomplete?.startPos ?? 0)) {
|
||||||
if (autocomplete) {
|
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 inp = evt.currentTarget
|
||||||
const fullKey = keyToString(evt)
|
const fullKey = keyToString(evt)
|
||||||
const sendKey = fullKey === "Enter" || fullKey === "Ctrl+Enter"
|
const sendKey = fullKey === "Enter" || fullKey === "Ctrl+Enter"
|
||||||
|
@ -295,7 +285,7 @@ const MessageComposer = () => {
|
||||||
|| autocomplete.selected !== undefined
|
|| autocomplete.selected !== undefined
|
||||||
|| !document.getElementById("composer-autocompletions")?.classList.contains("has-items")
|
|| !document.getElementById("composer-autocompletions")?.classList.contains("has-items")
|
||||||
)) {
|
)) {
|
||||||
sendMessage(evt)
|
onClickSend(evt)
|
||||||
} else if (autocomplete) {
|
} else if (autocomplete) {
|
||||||
let autocompleteUpdate: Partial<AutocompleteQuery> | null | undefined
|
let autocompleteUpdate: Partial<AutocompleteQuery> | null | undefined
|
||||||
if (fullKey === "Tab" || fullKey === "ArrowDown") {
|
if (fullKey === "Tab" || fullKey === "ArrowDown") {
|
||||||
|
@ -340,8 +330,8 @@ const MessageComposer = () => {
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
roomCtx.setEditing(null)
|
roomCtx.setEditing(null)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
const onChange = useEvent((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const onChange = (evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setState({ text: evt.target.value })
|
setState({ text: evt.target.value })
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (evt.target.value !== "" && typingSentAt.current + 5_000 < now) {
|
if (evt.target.value !== "" && typingSentAt.current + 5_000 < now) {
|
||||||
|
@ -358,7 +348,7 @@ const MessageComposer = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onComposerCaretChange(evt, evt.target.value)
|
onComposerCaretChange(evt, evt.target.value)
|
||||||
})
|
}
|
||||||
const doUploadFile = useCallback((file: File | null | undefined) => {
|
const doUploadFile = useCallback((file: File | null | undefined) => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return
|
return
|
||||||
|
@ -380,10 +370,7 @@ const MessageComposer = () => {
|
||||||
.catch(err => window.alert("Failed to upload file: " + err))
|
.catch(err => window.alert("Failed to upload file: " + err))
|
||||||
.finally(() => setLoadingMedia(false))
|
.finally(() => setLoadingMedia(false))
|
||||||
}, [room])
|
}, [room])
|
||||||
const onAttachFile = useEvent(
|
const onPaste = (evt: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||||
(evt: React.ChangeEvent<HTMLInputElement>) => doUploadFile(evt.target.files?.[0]),
|
|
||||||
)
|
|
||||||
const onPaste = useEvent((evt: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
const file = evt.clipboardData?.files?.[0]
|
const file = evt.clipboardData?.files?.[0]
|
||||||
const text = evt.clipboardData.getData("text/plain")
|
const text = evt.clipboardData.getData("text/plain")
|
||||||
const input = evt.currentTarget
|
const input = evt.currentTarget
|
||||||
|
@ -400,7 +387,7 @@ const MessageComposer = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
})
|
}
|
||||||
// To ensure the cursor jumps to the end, do this in an effect rather than as the initial value of useState
|
// 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
|
// To try to avoid the input bar flashing, use useLayoutEffect instead of useEffect
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
@ -450,7 +437,6 @@ const MessageComposer = () => {
|
||||||
draftStore.set(room.roomID, state)
|
draftStore.set(room.roomID, state)
|
||||||
}
|
}
|
||||||
}, [roomCtx, room, state, editing])
|
}, [roomCtx, room, state, editing])
|
||||||
const openFilePicker = useCallback(() => fileInput.current!.click(), [])
|
|
||||||
const clearMedia = useCallback(() => setState({ media: null, location: null }), [])
|
const clearMedia = useCallback(() => setState({ media: null, location: null }), [])
|
||||||
const onChangeLocation = useCallback((location: ComposerLocationValue) => setState({ location }), [])
|
const onChangeLocation = useCallback((location: ComposerLocationValue) => setState({ location }), [])
|
||||||
const closeReply = useCallback((evt: React.MouseEvent) => {
|
const closeReply = useCallback((evt: React.MouseEvent) => {
|
||||||
|
@ -461,56 +447,6 @@ const MessageComposer = () => {
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
roomCtx.setEditing(null)
|
roomCtx.setEditing(null)
|
||||||
}, [roomCtx])
|
}, [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)
|
const Autocompleter = getAutocompleter(autocomplete, client, room)
|
||||||
let mediaDisabledTitle: string | undefined
|
let mediaDisabledTitle: string | undefined
|
||||||
let stickerDisabledTitle: string | undefined
|
let stickerDisabledTitle: string | undefined
|
||||||
|
@ -533,7 +469,57 @@ const MessageComposer = () => {
|
||||||
} else if (state.text && !editing) {
|
} else if (state.text && !editing) {
|
||||||
stickerDisabledTitle = "You can't attach a sticker to a message with text"
|
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 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 <>
|
return <>
|
||||||
<button onClick={openEmojiPicker} title="Add emoji"><EmojiIcon/>{includeText && "Emoji"}</button>
|
<button onClick={openEmojiPicker} title="Add emoji"><EmojiIcon/>{includeText && "Emoji"}</button>
|
||||||
<button
|
<button
|
||||||
|
@ -556,13 +542,13 @@ const MessageComposer = () => {
|
||||||
title={locationDisabledTitle ?? "Add location"}
|
title={locationDisabledTitle ?? "Add location"}
|
||||||
><LocationIcon/>{includeText && "Location"}</button>
|
><LocationIcon/>{includeText && "Location"}</button>
|
||||||
<button
|
<button
|
||||||
onClick={openFilePicker}
|
onClick={() => fileInput.current!.click()}
|
||||||
disabled={!!mediaDisabledTitle}
|
disabled={!!mediaDisabledTitle}
|
||||||
title={mediaDisabledTitle ?? "Add file attachment"}
|
title={mediaDisabledTitle ?? "Add file attachment"}
|
||||||
><AttachIcon/>{includeText && "File"}</button>
|
><AttachIcon/>{includeText && "File"}</button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
const openButtonsModal = useEvent(() => {
|
const openButtonsModal = () => {
|
||||||
const style: CSSProperties = getEmojiPickerStyle()
|
const style: CSSProperties = getEmojiPickerStyle()
|
||||||
style.left = style.right
|
style.left = style.right
|
||||||
delete style.right
|
delete style.right
|
||||||
|
@ -571,7 +557,7 @@ const MessageComposer = () => {
|
||||||
{makeAttachmentButtons(true)}
|
{makeAttachmentButtons(true)}
|
||||||
</div>,
|
</div>,
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
const inlineButtons = state.text === "" || window.innerWidth > 720
|
const inlineButtons = state.text === "" || window.innerWidth > 720
|
||||||
const showSendButton = canSend || window.innerWidth > 720
|
const showSendButton = canSend || window.innerWidth > 720
|
||||||
const disableClearMedia = editing && state.media?.msgtype === "m.sticker"
|
const disableClearMedia = editing && state.media?.msgtype === "m.sticker"
|
||||||
|
@ -625,49 +611,19 @@ const MessageComposer = () => {
|
||||||
/>
|
/>
|
||||||
{inlineButtons && makeAttachmentButtons()}
|
{inlineButtons && makeAttachmentButtons()}
|
||||||
{showSendButton && <button
|
{showSendButton && <button
|
||||||
onClick={sendMessage}
|
onClick={onClickSend}
|
||||||
disabled={!canSend || loadingMedia}
|
disabled={!canSend || loadingMedia}
|
||||||
title="Send message"
|
title="Send message"
|
||||||
><SendIcon/></button>}
|
><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>
|
||||||
</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
|
export default MessageComposer
|
||||||
|
|
|
@ -13,11 +13,10 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { stringToRoomStateGUID } from "@/api/types"
|
||||||
import useContentVisibility from "@/util/contentvisibility.ts"
|
import useContentVisibility from "@/util/contentvisibility.ts"
|
||||||
import { CATEGORY_FREQUENTLY_USED, CustomEmojiPack, Emoji, categories } from "@/util/emoji"
|
import { CATEGORY_FREQUENTLY_USED, CustomEmojiPack, Emoji, categories } from "@/util/emoji"
|
||||||
import useEvent from "@/util/useEvent.ts"
|
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import renderEmoji from "./renderEmoji.tsx"
|
import renderEmoji from "./renderEmoji.tsx"
|
||||||
|
|
||||||
|
@ -56,27 +55,25 @@ export const EmojiGroup = ({
|
||||||
}
|
}
|
||||||
return emoji
|
return emoji
|
||||||
}
|
}
|
||||||
const onClickEmoji = useEvent((evt: React.MouseEvent<HTMLButtonElement>) =>
|
const onMouseOverEmoji = setPreviewEmoji && ((evt: React.MouseEvent<HTMLButtonElement>) =>
|
||||||
onSelect(getEmojiFromAttrs(evt.currentTarget)))
|
setPreviewEmoji(getEmojiFromAttrs(evt.currentTarget)))
|
||||||
const onMouseOverEmoji = useEvent((evt: React.MouseEvent<HTMLButtonElement>) =>
|
const onMouseOutEmoji = setPreviewEmoji && (() => setPreviewEmoji(undefined))
|
||||||
setPreviewEmoji?.(getEmojiFromAttrs(evt.currentTarget)))
|
const onClickSubscribePack = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
const onMouseOutEmoji = useCallback(() => setPreviewEmoji?.(undefined), [setPreviewEmoji])
|
|
||||||
const onClickSubscribePack = useEvent((evt: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id"))
|
const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id"))
|
||||||
if (!guid) {
|
if (!guid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
client.subscribeToEmojiPack(guid, true)
|
client.subscribeToEmojiPack(guid, true)
|
||||||
.catch(err => window.alert(`Failed to subscribe to emoji pack: ${err}`))
|
.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"))
|
const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id"))
|
||||||
if (!guid) {
|
if (!guid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
client.subscribeToEmojiPack(guid, false)
|
client.subscribeToEmojiPack(guid, false)
|
||||||
.catch(err => window.alert(`Failed to unsubscribe from emoji pack: ${err}`))
|
.catch(err => window.alert(`Failed to unsubscribe from emoji pack: ${err}`))
|
||||||
})
|
}
|
||||||
|
|
||||||
let categoryName: string
|
let categoryName: string
|
||||||
if (typeof categoryID === "number") {
|
if (typeof categoryID === "number") {
|
||||||
|
@ -112,7 +109,7 @@ export const EmojiGroup = ({
|
||||||
data-emoji-index={idx}
|
data-emoji-index={idx}
|
||||||
onMouseOver={onMouseOverEmoji}
|
onMouseOver={onMouseOverEmoji}
|
||||||
onMouseOut={onMouseOutEmoji}
|
onMouseOut={onMouseOutEmoji}
|
||||||
onClick={onClickEmoji}
|
onClick={evt => onSelect(getEmojiFromAttrs(evt.currentTarget))}
|
||||||
title={emoji.t}
|
title={emoji.t}
|
||||||
>{renderEmoji(emoji)}</button>) : null}
|
>{renderEmoji(emoji)}</button>) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { RoomStateStore, useCustomEmojis } from "@/api/statestore"
|
||||||
import { roomStateGUIDToString } from "@/api/types"
|
import { roomStateGUIDToString } from "@/api/types"
|
||||||
import { CATEGORY_FREQUENTLY_USED, Emoji, PartialEmoji, categories, useFilteredEmojis } from "@/util/emoji"
|
import { CATEGORY_FREQUENTLY_USED, Emoji, PartialEmoji, categories, useFilteredEmojis } from "@/util/emoji"
|
||||||
import { isMobileDevice } from "@/util/ismobile.ts"
|
import { isMobileDevice } from "@/util/ismobile.ts"
|
||||||
import useEvent from "@/util/useEvent.ts"
|
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import { ModalCloseContext } from "../modal/Modal.tsx"
|
import { ModalCloseContext } from "../modal/Modal.tsx"
|
||||||
import { EmojiGroup } from "./EmojiGroup.tsx"
|
import { EmojiGroup } from "./EmojiGroup.tsx"
|
||||||
|
@ -72,7 +71,6 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe
|
||||||
frequentlyUsed: client.store.frequentlyUsedEmoji,
|
frequentlyUsed: client.store.frequentlyUsedEmoji,
|
||||||
customEmojiPacks,
|
customEmojiPacks,
|
||||||
})
|
})
|
||||||
const clearQuery = useCallback(() => setQuery(""), [])
|
|
||||||
const close = closeOnSelect ? use(ModalCloseContext) : null
|
const close = closeOnSelect ? use(ModalCloseContext) : null
|
||||||
const onSelectWrapped = useCallback((emoji?: PartialEmoji) => {
|
const onSelectWrapped = useCallback((emoji?: PartialEmoji) => {
|
||||||
if (!emoji) {
|
if (!emoji) {
|
||||||
|
@ -85,12 +83,10 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe
|
||||||
}
|
}
|
||||||
close?.()
|
close?.()
|
||||||
}, [onSelect, selected, client, close])
|
}, [onSelect, selected, client, close])
|
||||||
const onClickFreeformReact = useEvent(() => onSelectWrapped({ u: query }))
|
const onClickCategoryButton = (evt: React.MouseEvent) => {
|
||||||
const onChangeQuery = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setQuery(evt.target.value), [])
|
|
||||||
const onClickCategoryButton = useCallback((evt: React.MouseEvent) => {
|
|
||||||
const categoryID = evt.currentTarget.getAttribute("data-category-id")!
|
const categoryID = evt.currentTarget.getAttribute("data-category-id")!
|
||||||
document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView()
|
document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView()
|
||||||
}, [])
|
}
|
||||||
return <div className="emoji-picker" style={style}>
|
return <div className="emoji-picker" style={style}>
|
||||||
<div className="emoji-category-bar" ref={emojiCategoryBarRef}>
|
<div className="emoji-category-bar" ref={emojiCategoryBarRef}>
|
||||||
<button
|
<button
|
||||||
|
@ -123,12 +119,12 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe
|
||||||
<div className="emoji-search">
|
<div className="emoji-search">
|
||||||
<input
|
<input
|
||||||
autoFocus={!isMobileDevice}
|
autoFocus={!isMobileDevice}
|
||||||
onChange={onChangeQuery}
|
onChange={evt => setQuery(evt.target.value)}
|
||||||
value={query}
|
value={query}
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search emojis"
|
placeholder="Search emojis"
|
||||||
/>
|
/>
|
||||||
<button onClick={clearQuery} disabled={query === ""}>
|
<button onClick={() => setQuery("")} disabled={query === ""}>
|
||||||
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,7 +151,7 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe
|
||||||
})}
|
})}
|
||||||
{allowFreeform && query && <button
|
{allowFreeform && query && <button
|
||||||
className="freeform-react"
|
className="freeform-react"
|
||||||
onClick={onClickFreeformReact}
|
onClick={() => onSelectWrapped({ u: query })}
|
||||||
>React with "{query}"</button>}
|
>React with "{query}"</button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { RoomStateStore, usePreference } from "@/api/statestore"
|
||||||
import { MediaMessageEventContent } from "@/api/types"
|
import { MediaMessageEventContent } from "@/api/types"
|
||||||
import { isMobileDevice } from "@/util/ismobile.ts"
|
import { isMobileDevice } from "@/util/ismobile.ts"
|
||||||
|
@ -36,13 +36,11 @@ const GIFPicker = ({ style, onSelect, room }: MediaPickerProps) => {
|
||||||
const [results, setResults] = useState<GIF[]>([])
|
const [results, setResults] = useState<GIF[]>([])
|
||||||
const [error, setError] = useState<unknown>()
|
const [error, setError] = useState<unknown>()
|
||||||
const close = use(ModalCloseContext)
|
const close = use(ModalCloseContext)
|
||||||
const clearQuery = useCallback(() => setQuery(""), [])
|
|
||||||
const onChangeQuery = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setQuery(evt.target.value), [])
|
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const provider = usePreference(client.store, room, "gif_provider")
|
const provider = usePreference(client.store, room, "gif_provider")
|
||||||
const providerName = provider.slice(0, 1).toUpperCase() + provider.slice(1)
|
const providerName = provider.slice(0, 1).toUpperCase() + provider.slice(1)
|
||||||
// const reuploadGIFs = room.preferences.reupload_gifs
|
// 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")
|
const idx = evt.currentTarget.getAttribute("data-gif-index")
|
||||||
if (!idx) {
|
if (!idx) {
|
||||||
return
|
return
|
||||||
|
@ -64,7 +62,7 @@ const GIFPicker = ({ style, onSelect, room }: MediaPickerProps) => {
|
||||||
url: gif.proxied_mxc,
|
url: gif.proxied_mxc,
|
||||||
})
|
})
|
||||||
close()
|
close()
|
||||||
}, [onSelect, close, results])
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
if (trendingCache.has(provider)) {
|
if (trendingCache.has(provider)) {
|
||||||
|
@ -106,12 +104,12 @@ const GIFPicker = ({ style, onSelect, room }: MediaPickerProps) => {
|
||||||
<div className="gif-search">
|
<div className="gif-search">
|
||||||
<input
|
<input
|
||||||
autoFocus={!isMobileDevice}
|
autoFocus={!isMobileDevice}
|
||||||
onChange={onChangeQuery}
|
onChange={evt => setQuery(evt.target.value)}
|
||||||
value={query}
|
value={query}
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={`Search ${providerName}`}
|
placeholder={`Search ${providerName}`}
|
||||||
/>
|
/>
|
||||||
<button onClick={clearQuery} disabled={query === ""}>
|
<button onClick={() => setQuery("")} disabled={query === ""}>
|
||||||
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,7 +39,6 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => {
|
||||||
customEmojiPacks,
|
customEmojiPacks,
|
||||||
stickers: true,
|
stickers: true,
|
||||||
})
|
})
|
||||||
const clearQuery = useCallback(() => setQuery(""), [])
|
|
||||||
const close = use(ModalCloseContext)
|
const close = use(ModalCloseContext)
|
||||||
const onSelectWrapped = useCallback((emoji?: Emoji) => {
|
const onSelectWrapped = useCallback((emoji?: Emoji) => {
|
||||||
if (!emoji) {
|
if (!emoji) {
|
||||||
|
@ -53,11 +52,10 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => {
|
||||||
})
|
})
|
||||||
close()
|
close()
|
||||||
}, [onSelect, close])
|
}, [onSelect, close])
|
||||||
const onChangeQuery = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setQuery(evt.target.value), [])
|
const onClickCategoryButton = (evt: React.MouseEvent) => {
|
||||||
const onClickCategoryButton = useCallback((evt: React.MouseEvent) => {
|
|
||||||
const categoryID = evt.currentTarget.getAttribute("data-category-id")!
|
const categoryID = evt.currentTarget.getAttribute("data-category-id")!
|
||||||
document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView()
|
document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView()
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
return <div className="sticker-picker" style={style}>
|
return <div className="sticker-picker" style={style}>
|
||||||
<div className="emoji-category-bar" ref={emojiCategoryBarRef}>
|
<div className="emoji-category-bar" ref={emojiCategoryBarRef}>
|
||||||
|
@ -76,12 +74,12 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => {
|
||||||
<div className="emoji-search">
|
<div className="emoji-search">
|
||||||
<input
|
<input
|
||||||
autoFocus={!isMobileDevice}
|
autoFocus={!isMobileDevice}
|
||||||
onChange={onChangeQuery}
|
onChange={evt => setQuery(evt.target.value)}
|
||||||
value={query}
|
value={query}
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search stickers"
|
placeholder="Search stickers"
|
||||||
/>
|
/>
|
||||||
<button onClick={clearQuery} disabled={query === ""}>
|
<button onClick={() => setQuery("")} disabled={query === ""}>
|
||||||
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,10 +13,9 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 * as beeper from "@/api/beeper.ts"
|
||||||
import type Client from "@/api/client.ts"
|
import type Client from "@/api/client.ts"
|
||||||
import useEvent from "@/util/useEvent.ts"
|
|
||||||
|
|
||||||
interface BeeperLoginProps {
|
interface BeeperLoginProps {
|
||||||
domain: string
|
domain: string
|
||||||
|
@ -29,18 +28,18 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => {
|
||||||
const [code, setCode] = useState("")
|
const [code, setCode] = useState("")
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
|
||||||
const onChangeEmail = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
|
const onChangeEmail = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setEmail(evt.target.value)
|
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)
|
let codeDigits = evt.target.value.replace(/\D/g, "").slice(0, 6)
|
||||||
if (codeDigits.length > 3) {
|
if (codeDigits.length > 3) {
|
||||||
codeDigits = codeDigits.slice(0, 3) + " " + codeDigits.slice(3)
|
codeDigits = codeDigits.slice(0, 3) + " " + codeDigits.slice(3)
|
||||||
}
|
}
|
||||||
setCode(codeDigits)
|
setCode(codeDigits)
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
const requestCode = useEvent((evt: React.FormEvent) => {
|
const requestCode = (evt: React.FormEvent) => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
beeper.doStartLogin(domain).then(
|
beeper.doStartLogin(domain).then(
|
||||||
request => beeper.doRequestCode(domain, request, email).then(
|
request => beeper.doRequestCode(domain, request, email).then(
|
||||||
|
@ -49,8 +48,8 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => {
|
||||||
),
|
),
|
||||||
err => setError(`Failed to start login: ${err}`),
|
err => setError(`Failed to start login: ${err}`),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
const submitCode = useEvent((evt: React.FormEvent) => {
|
const submitCode = (evt: React.FormEvent) => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
beeper.doSubmitCode(domain, requestID, code).then(
|
beeper.doSubmitCode(domain, requestID, code).then(
|
||||||
token => {
|
token => {
|
||||||
|
@ -61,7 +60,7 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => {
|
||||||
},
|
},
|
||||||
err => setError(`Failed to submit code: ${err}`),
|
err => setError(`Failed to submit code: ${err}`),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
return <form onSubmit={requestID ? submitCode : requestCode} className="beeper-login">
|
return <form onSubmit={requestID ? submitCode : requestCode} className="beeper-login">
|
||||||
<h2>Beeper email login</h2>
|
<h2>Beeper email login</h2>
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
import React, { useCallback, useEffect, useState } from "react"
|
import React, { useCallback, useEffect, useState } from "react"
|
||||||
import type Client from "@/api/client.ts"
|
import type Client from "@/api/client.ts"
|
||||||
import type { ClientState } from "@/api/types"
|
import type { ClientState } from "@/api/types"
|
||||||
import useEvent from "@/util/useEvent.ts"
|
|
||||||
import BeeperLogin from "./BeeperLogin.tsx"
|
import BeeperLogin from "./BeeperLogin.tsx"
|
||||||
import "./LoginScreen.css"
|
import "./LoginScreen.css"
|
||||||
|
|
||||||
|
@ -34,7 +33,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => {
|
||||||
const [loginFlows, setLoginFlows] = useState<string[] | null>(null)
|
const [loginFlows, setLoginFlows] = useState<string[] | null>(null)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
|
||||||
const loginSSO = useEvent(() => {
|
const loginSSO = () => {
|
||||||
fetch("_gomuks/sso", {
|
fetch("_gomuks/sso", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ homeserver_url: homeserverURL }),
|
body: JSON.stringify({ homeserver_url: homeserverURL }),
|
||||||
|
@ -53,9 +52,9 @@ export const LoginScreen = ({ client }: LoginScreenProps) => {
|
||||||
},
|
},
|
||||||
err => setError(`Failed to start SSO login: ${err}`),
|
err => setError(`Failed to start SSO login: ${err}`),
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
const login = useEvent((evt: React.FormEvent) => {
|
const login = (evt: React.FormEvent) => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
if (!loginFlows) {
|
if (!loginFlows) {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
@ -67,7 +66,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => {
|
||||||
err => setError(err.toString()),
|
err => setError(err.toString()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
const resolveLoginFlows = useCallback((serverURL: string) => {
|
const resolveLoginFlows = useCallback((serverURL: string) => {
|
||||||
client.rpc.getLoginFlows(serverURL).then(
|
client.rpc.getLoginFlows(serverURL).then(
|
||||||
|
@ -108,16 +107,10 @@ export const LoginScreen = ({ client }: LoginScreenProps) => {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
}, [homeserverURL, loginFlows, resolveLoginFlows])
|
}, [homeserverURL, loginFlows, resolveLoginFlows])
|
||||||
const onChangeUsername = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
|
const onChangeHomeserverURL = (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>) => {
|
|
||||||
setLoginFlows(null)
|
setLoginFlows(null)
|
||||||
setHomeserverURL(evt.target.value)
|
setHomeserverURL(evt.target.value)
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
const supportsSSO = loginFlows?.includes("m.login.sso") ?? false
|
const supportsSSO = loginFlows?.includes("m.login.sso") ?? false
|
||||||
const supportsPassword = loginFlows?.includes("m.login.password")
|
const supportsPassword = loginFlows?.includes("m.login.password")
|
||||||
|
@ -130,7 +123,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => {
|
||||||
id="mxlogin-username"
|
id="mxlogin-username"
|
||||||
placeholder="User ID"
|
placeholder="User ID"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={onChangeUsername}
|
onChange={evt => setUsername(evt.target.value)}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -144,7 +137,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => {
|
||||||
id="mxlogin-password"
|
id="mxlogin-password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={onChangePassword}
|
onChange={evt => setPassword(evt.target.value)}
|
||||||
/>}
|
/>}
|
||||||
<div className="buttons">
|
<div className="buttons">
|
||||||
{supportsSSO && <button
|
{supportsSSO && <button
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { LoginScreenProps } from "./LoginScreen.tsx"
|
||||||
import "./LoginScreen.css"
|
import "./LoginScreen.css"
|
||||||
|
|
||||||
|
@ -24,13 +24,13 @@ export const VerificationScreen = ({ client, clientState }: LoginScreenProps) =>
|
||||||
const [recoveryKey, setRecoveryKey] = useState("")
|
const [recoveryKey, setRecoveryKey] = useState("")
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
|
||||||
const verify = useCallback((evt: React.FormEvent) => {
|
const verify = (evt: React.FormEvent) => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
client.rpc.verify(recoveryKey).then(
|
client.rpc.verify(recoveryKey).then(
|
||||||
() => {},
|
() => {},
|
||||||
err => setError(err.toString()),
|
err => setError(err.toString()),
|
||||||
)
|
)
|
||||||
}, [recoveryKey, client])
|
}
|
||||||
|
|
||||||
return <main className="matrix-login">
|
return <main className="matrix-login">
|
||||||
<h1>gomuks web</h1>
|
<h1>gomuks web</h1>
|
||||||
|
|
|
@ -45,7 +45,7 @@ export const ModalWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
const onKeyWrapper = useCallback((evt: React.KeyboardEvent<HTMLDivElement>) => {
|
const onKeyWrapper = (evt: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (evt.key === "Escape") {
|
if (evt.key === "Escape") {
|
||||||
setState(null)
|
setState(null)
|
||||||
if (history.state?.modal) {
|
if (history.state?.modal) {
|
||||||
|
@ -53,7 +53,7 @@ export const ModalWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
}, [])
|
}
|
||||||
const openModal = useCallback((newState: ModalState) => {
|
const openModal = useCallback((newState: ModalState) => {
|
||||||
if (!history.state?.modal) {
|
if (!history.state?.modal) {
|
||||||
history.pushState({ ...(history.state ?? {}), modal: true }, "")
|
history.pushState({ ...(history.state ?? {}), modal: true }, "")
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { getAvatarURL } from "@/api/media.ts"
|
||||||
import { MemDBEvent, MemberEventContent } from "@/api/types"
|
import { MemDBEvent, MemberEventContent } from "@/api/types"
|
||||||
import { getDisplayname } from "@/util/validation.ts"
|
import { getDisplayname } from "@/util/validation.ts"
|
||||||
|
@ -45,8 +45,6 @@ const MemberRow = ({ evt, onClick }: MemberRowProps) => {
|
||||||
const MemberList = () => {
|
const MemberList = () => {
|
||||||
const [filter, setFilter] = useState("")
|
const [filter, setFilter] = useState("")
|
||||||
const [limit, setLimit] = useState(30)
|
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)
|
const roomCtx = use(RoomContext)
|
||||||
if (roomCtx?.store && !roomCtx?.store.membersRequested && !roomCtx?.store.fullMembersLoaded) {
|
if (roomCtx?.store && !roomCtx?.store.membersRequested && !roomCtx?.store.fullMembersLoaded) {
|
||||||
roomCtx.store.membersRequested = true
|
roomCtx.store.membersRequested = true
|
||||||
|
@ -69,10 +67,15 @@ const MemberList = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <>
|
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">
|
<div className="member-list">
|
||||||
{members}
|
{members}
|
||||||
{memberEvents.length > limit ? <button onClick={increaseLimit}>
|
{memberEvents.length > limit ? <button onClick={() => setLimit(limit => limit + 50)}>
|
||||||
and {memberEvents.length - limit} others…
|
and {memberEvents.length - limit} others…
|
||||||
</button> : null}
|
</button> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { ScaleLoader } from "react-spinners"
|
||||||
import Client from "@/api/client.ts"
|
import Client from "@/api/client.ts"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomStateStore } from "@/api/statestore"
|
||||||
|
@ -34,17 +34,19 @@ const DeviceList = ({ client, room, userID }: DeviceListProps) => {
|
||||||
const [view, setEncryptionInfo] = useState<ProfileEncryptionInfo | null>(null)
|
const [view, setEncryptionInfo] = useState<ProfileEncryptionInfo | null>(null)
|
||||||
const [errors, setErrors] = useState<string[] | null>(null)
|
const [errors, setErrors] = useState<string[] | null>(null)
|
||||||
const [trackChangePending, startTransition] = useTransition()
|
const [trackChangePending, startTransition] = useTransition()
|
||||||
const doTrackDeviceList = useCallback(() => {
|
const doTrackDeviceList = () => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await client.rpc.trackUserDevices(userID)
|
const resp = await client.rpc.trackUserDevices(userID)
|
||||||
|
startTransition(() => {
|
||||||
setEncryptionInfo(resp)
|
setEncryptionInfo(resp)
|
||||||
setErrors(resp.errors)
|
setErrors(resp.errors)
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrors([`${err}`])
|
startTransition(() => setErrors([`${err}`]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [client, userID])
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEncryptionInfo(null)
|
setEncryptionInfo(null)
|
||||||
setErrors(null)
|
setErrors(null)
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 type { RoomID } from "@/api/types"
|
||||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
import reverseMap from "@/util/reversemap.ts"
|
import reverseMap from "@/util/reversemap.ts"
|
||||||
|
@ -38,18 +38,18 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
|
||||||
const [roomFilter, setRoomFilter] = useState("")
|
const [roomFilter, setRoomFilter] = useState("")
|
||||||
const [realRoomFilter, setRealRoomFilter] = useState("")
|
const [realRoomFilter, setRealRoomFilter] = useState("")
|
||||||
|
|
||||||
const updateRoomFilter = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
|
const updateRoomFilter = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setRoomFilter(evt.target.value)
|
setRoomFilter(evt.target.value)
|
||||||
client.store.currentRoomListFilter = toSearchableString(evt.target.value)
|
client.store.currentRoomListFilter = toSearchableString(evt.target.value)
|
||||||
setRealRoomFilter(client.store.currentRoomListFilter)
|
setRealRoomFilter(client.store.currentRoomListFilter)
|
||||||
}, [client])
|
}
|
||||||
const clearQuery = useCallback(() => {
|
const clearQuery = () => {
|
||||||
setRoomFilter("")
|
setRoomFilter("")
|
||||||
client.store.currentRoomListFilter = ""
|
client.store.currentRoomListFilter = ""
|
||||||
setRealRoomFilter("")
|
setRealRoomFilter("")
|
||||||
roomFilterRef.current?.focus()
|
roomFilterRef.current?.focus()
|
||||||
}, [client])
|
}
|
||||||
const onKeyDown = useCallback((evt: React.KeyboardEvent<HTMLInputElement>) => {
|
const onKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
const key = keyToString(evt)
|
const key = keyToString(evt)
|
||||||
if (key === "Escape") {
|
if (key === "Escape") {
|
||||||
clearQuery()
|
clearQuery()
|
||||||
|
@ -62,7 +62,7 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
}
|
}
|
||||||
}, [mainScreen, client.store, clearQuery])
|
}
|
||||||
|
|
||||||
return <div className="room-list-wrapper">
|
return <div className="room-list-wrapper">
|
||||||
<div className="room-search-wrapper">
|
<div className="room-search-wrapper">
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { ScaleLoader } from "react-spinners"
|
||||||
import { getAvatarURL, getRoomAvatarURL } from "@/api/media.ts"
|
import { getAvatarURL, getRoomAvatarURL } from "@/api/media.ts"
|
||||||
import { InvitedRoomStore } from "@/api/statestore/invitedroom.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 [loading, setLoading] = useState(false)
|
||||||
const [buttonClicked, setButtonClicked] = useState(false)
|
const [buttonClicked, setButtonClicked] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const doJoinRoom = useCallback(() => {
|
const doJoinRoom = () => {
|
||||||
let realVia = via
|
let realVia = via
|
||||||
if (!via?.length && invite?.invited_by) {
|
if (!via?.length && invite?.invited_by) {
|
||||||
realVia = [getServerName(invite.invited_by)]
|
realVia = [getServerName(invite.invited_by)]
|
||||||
|
@ -54,8 +54,8 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => {
|
||||||
setButtonClicked(false)
|
setButtonClicked(false)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [client, roomID, via, alias, invite])
|
}
|
||||||
const doRejectInvite = useCallback(() => {
|
const doRejectInvite = () => {
|
||||||
setButtonClicked(true)
|
setButtonClicked(true)
|
||||||
client.rpc.leaveRoom(roomID).then(
|
client.rpc.leaveRoom(roomID).then(
|
||||||
() => {
|
() => {
|
||||||
|
@ -67,7 +67,7 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => {
|
||||||
setButtonClicked(false)
|
setButtonClicked(false)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [client, mainScreen, roomID])
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSummary(null)
|
setSummary(null)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { getRoomAvatarURL } from "@/api/media.ts"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomStateStore } from "@/api/statestore"
|
||||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
|
@ -35,14 +35,14 @@ const RoomViewHeader = ({ room }: RoomViewHeaderProps) => {
|
||||||
const roomMeta = useEventAsState(room.meta)
|
const roomMeta = useEventAsState(room.meta)
|
||||||
const mainScreen = use(MainScreenContext)
|
const mainScreen = use(MainScreenContext)
|
||||||
const openModal = use(ModalContext)
|
const openModal = use(ModalContext)
|
||||||
const openSettings = useCallback(() => {
|
const openSettings = () => {
|
||||||
openModal({
|
openModal({
|
||||||
dimmed: true,
|
dimmed: true,
|
||||||
boxed: true,
|
boxed: true,
|
||||||
innerBoxClass: "settings-view",
|
innerBoxClass: "settings-view",
|
||||||
content: <SettingsView room={room} />,
|
content: <SettingsView room={room} />,
|
||||||
})
|
})
|
||||||
}, [room, openModal])
|
}
|
||||||
return <div className="room-header">
|
return <div className="room-header">
|
||||||
<button className="back" onClick={mainScreen.clearActiveRoom}><BackIcon/></button>
|
<button className="back" onClick={mainScreen.clearActiveRoom}><BackIcon/></button>
|
||||||
<img
|
<img
|
||||||
|
|
|
@ -45,52 +45,39 @@ interface PreferenceCellProps<T extends PreferenceValueType> {
|
||||||
inheritedValue: T
|
inheritedValue: T
|
||||||
}
|
}
|
||||||
|
|
||||||
const useRemover = (
|
const makeRemover = (
|
||||||
context: PreferenceContext, setPref: SetPrefFunc, name: keyof Preferences, value: PreferenceValueType | undefined,
|
context: PreferenceContext, setPref: SetPrefFunc, name: keyof Preferences, value: PreferenceValueType | undefined,
|
||||||
) => {
|
) => {
|
||||||
const onClear = useCallback(() => {
|
|
||||||
setPref(context, name, undefined)
|
|
||||||
}, [setPref, context, name])
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return null
|
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 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">
|
return <div className="preference boolean-preference">
|
||||||
<Toggle checked={value ?? inheritedValue} onChange={onChange}/>
|
<Toggle checked={value ?? inheritedValue} onChange={evt => setPref(context, name, evt.target.checked)}/>
|
||||||
{useRemover(context, setPref, name, value)}
|
{makeRemover(context, setPref, name, value)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextPreferenceCell = ({ context, name, setPref, value, inheritedValue }: PreferenceCellProps<string>) => {
|
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">
|
return <div className="preference string-preference">
|
||||||
<input value={value ?? inheritedValue} onChange={onChange}/>
|
<input value={value ?? inheritedValue} onChange={evt => setPref(context, name, evt.target.value)}/>
|
||||||
{useRemover(context, setPref, name, value)}
|
{makeRemover(context, setPref, name, value)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectPreferenceCell = ({ context, name, pref, setPref, value, inheritedValue }: PreferenceCellProps<string>) => {
|
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) {
|
if (!pref.allowedValues) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return <div className="preference select-preference">
|
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 =>
|
{pref.allowedValues.map(value =>
|
||||||
<option key={value} value={value}>{value}</option>)}
|
<option key={value} value={value}>{value}</option>)}
|
||||||
</select>
|
</select>
|
||||||
{remover}
|
{makeRemover(context, setPref, name, value)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +173,7 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const appliedContext = getActiveCSSContext(client, room)
|
const appliedContext = getActiveCSSContext(client, room)
|
||||||
const [context, setContext] = useState(appliedContext)
|
const [context, setContext] = useState(appliedContext)
|
||||||
const getContextText = useCallback((context: PreferenceContext) => {
|
const getContextText = (context: PreferenceContext) => {
|
||||||
if (context === PreferenceContext.Account) {
|
if (context === PreferenceContext.Account) {
|
||||||
return client.store.serverPreferenceCache.custom_css
|
return client.store.serverPreferenceCache.custom_css
|
||||||
} else if (context === PreferenceContext.Device) {
|
} else if (context === PreferenceContext.Device) {
|
||||||
|
@ -196,17 +183,17 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta
|
||||||
} else if (context === PreferenceContext.RoomDevice) {
|
} else if (context === PreferenceContext.RoomDevice) {
|
||||||
return room.localPreferenceCache.custom_css
|
return room.localPreferenceCache.custom_css
|
||||||
}
|
}
|
||||||
}, [client, room])
|
}
|
||||||
const origText = getContextText(context)
|
const origText = getContextText(context)
|
||||||
const [text, setText] = useState(origText ?? "")
|
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
|
const newContext = evt.target.value as PreferenceContext
|
||||||
setContext(newContext)
|
setContext(newContext)
|
||||||
setText(getContextText(newContext) ?? "")
|
setText(getContextText(newContext) ?? "")
|
||||||
}, [getContextText])
|
}
|
||||||
const onChangeText = useCallback((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const onChangeText = (evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setText(evt.target.value)
|
setText(evt.target.value)
|
||||||
}, [])
|
}
|
||||||
const onSave = useEvent(() => {
|
const onSave = useEvent(() => {
|
||||||
if (vscodeOpen) {
|
if (vscodeOpen) {
|
||||||
setText(vscodeContentRef.current)
|
setText(vscodeContentRef.current)
|
||||||
|
@ -215,18 +202,18 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta
|
||||||
setPref(context, "custom_css", text)
|
setPref(context, "custom_css", text)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const onDelete = useEvent(() => {
|
const onDelete = () => {
|
||||||
setPref(context, "custom_css", undefined)
|
setPref(context, "custom_css", undefined)
|
||||||
setText("")
|
setText("")
|
||||||
})
|
}
|
||||||
const [vscodeOpen, setVSCodeOpen] = useState(false)
|
const [vscodeOpen, setVSCodeOpen] = useState(false)
|
||||||
const vscodeContentRef = useRef("")
|
const vscodeContentRef = useRef("")
|
||||||
const vscodeInitialContentRef = useRef("")
|
const vscodeInitialContentRef = useRef("")
|
||||||
const onClickVSCode = useEvent(() => {
|
const onClickVSCode = () => {
|
||||||
vscodeContentRef.current = text
|
vscodeContentRef.current = text
|
||||||
vscodeInitialContentRef.current = text
|
vscodeInitialContentRef.current = text
|
||||||
setVSCodeOpen(true)
|
setVSCodeOpen(true)
|
||||||
})
|
}
|
||||||
const closeVSCode = useCallback(() => {
|
const closeVSCode = useCallback(() => {
|
||||||
setVSCodeOpen(false)
|
setVSCodeOpen(false)
|
||||||
setText(vscodeContentRef.current)
|
setText(vscodeContentRef.current)
|
||||||
|
@ -296,7 +283,9 @@ const SettingsView = ({ room }: SettingsViewProps) => {
|
||||||
const roomMeta = useEventAsState(room.meta)
|
const roomMeta = useEventAsState(room.meta)
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const closeModal = use(ModalCloseContext)
|
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) {
|
if (context === PreferenceContext.Account) {
|
||||||
client.rpc.setAccountData("fi.mau.gomuks.preferences", {
|
client.rpc.setAccountData("fi.mau.gomuks.preferences", {
|
||||||
...client.store.serverPreferenceCache,
|
...client.store.serverPreferenceCache,
|
||||||
|
@ -321,15 +310,15 @@ const SettingsView = ({ room }: SettingsViewProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [client, room])
|
}, [client, room])
|
||||||
const onClickLogout = useCallback(() => {
|
const onClickLogout = () => {
|
||||||
if (window.confirm("Really log out and delete all local data?")) {
|
if (window.confirm("Really log out and delete all local data?")) {
|
||||||
client.logout().then(
|
client.logout().then(
|
||||||
() => console.info("Successfully logged out"),
|
() => console.info("Successfully logged out"),
|
||||||
err => window.alert(`Failed to log out: ${err}`),
|
err => window.alert(`Failed to log out: ${err}`),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [client])
|
}
|
||||||
const onClickLeave = useCallback(() => {
|
const onClickLeave = () => {
|
||||||
if (window.confirm(`Really leave ${room.meta.current.name}?`)) {
|
if (window.confirm(`Really leave ${room.meta.current.name}?`)) {
|
||||||
client.rpc.leaveRoom(room.roomID).then(
|
client.rpc.leaveRoom(room.roomID).then(
|
||||||
() => {
|
() => {
|
||||||
|
@ -339,7 +328,7 @@ const SettingsView = ({ room }: SettingsViewProps) => {
|
||||||
err => window.alert(`Failed to leave room: ${err}`),
|
err => window.alert(`Failed to leave room: ${err}`),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [client, room, closeModal])
|
}
|
||||||
usePreferences(client.store, room)
|
usePreferences(client.store, room)
|
||||||
const globalServer = client.store.serverPreferenceCache
|
const globalServer = client.store.serverPreferenceCache
|
||||||
const globalLocal = client.store.localPreferenceCache
|
const globalLocal = client.store.localPreferenceCache
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts"
|
||||||
import { useRoomMember } from "@/api/statestore"
|
import { useRoomMember } from "@/api/statestore"
|
||||||
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
||||||
|
@ -79,7 +79,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
|
||||||
const mainScreen = use(MainScreenContext)
|
const mainScreen = use(MainScreenContext)
|
||||||
const openModal = use(ModalContext)
|
const openModal = use(ModalContext)
|
||||||
const [forceContextMenuOpen, setForceContextMenuOpen] = useState(false)
|
const [forceContextMenuOpen, setForceContextMenuOpen] = useState(false)
|
||||||
const onContextMenu = useCallback((mouseEvt: React.MouseEvent) => {
|
const onContextMenu = (mouseEvt: React.MouseEvent) => {
|
||||||
const targetElem = mouseEvt.target as HTMLElement
|
const targetElem = mouseEvt.target as HTMLElement
|
||||||
if (
|
if (
|
||||||
!roomCtx.store.preferences.message_context_menu
|
!roomCtx.store.preferences.message_context_menu
|
||||||
|
@ -97,7 +97,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
|
||||||
style={getModalStyleFromMouse(mouseEvt, 9 * 40)}
|
style={getModalStyleFromMouse(mouseEvt, 9 * 40)}
|
||||||
/>,
|
/>,
|
||||||
})
|
})
|
||||||
}, [openModal, evt, roomCtx])
|
}
|
||||||
const memberEvt = useRoomMember(client, roomCtx.store, evt.sender)
|
const memberEvt = useRoomMember(client, roomCtx.store, evt.sender)
|
||||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||||
const BodyType = getBodyType(evt)
|
const BodyType = getBodyType(evt)
|
||||||
|
|
|
@ -46,13 +46,13 @@ const TimelineView = () => {
|
||||||
|
|
||||||
// When the user scrolls the timeline manually, remember if they were at the bottom,
|
// 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.
|
// so that we can keep them at the bottom when new events are added.
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = () => {
|
||||||
if (!timelineViewRef.current) {
|
if (!timelineViewRef.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const timelineView = timelineViewRef.current
|
const timelineView = timelineViewRef.current
|
||||||
roomCtx.scrolledToBottom = timelineView.scrollTop + timelineView.clientHeight + 1 >= timelineView.scrollHeight
|
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.
|
// Save the scroll height prior to updating the timeline, so that we can adjust the scroll position if needed.
|
||||||
if (timelineViewRef.current) {
|
if (timelineViewRef.current) {
|
||||||
oldScrollHeight.current = timelineViewRef.current.scrollHeight
|
oldScrollHeight.current = timelineViewRef.current.scrollHeight
|
||||||
|
|
|
@ -13,9 +13,9 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { MemDBEvent } from "@/api/types"
|
||||||
import useEvent from "@/util/useEvent.ts"
|
import { isMobileDevice } from "@/util/ismobile.ts"
|
||||||
import { ModalCloseContext } from "../../modal/Modal.tsx"
|
import { ModalCloseContext } from "../../modal/Modal.tsx"
|
||||||
import TimelineEvent from "../TimelineEvent.tsx"
|
import TimelineEvent from "../TimelineEvent.tsx"
|
||||||
|
|
||||||
|
@ -33,14 +33,11 @@ const ConfirmWithMessageModal = ({
|
||||||
}: ConfirmWithMessageProps) => {
|
}: ConfirmWithMessageProps) => {
|
||||||
const [reason, setReason] = useState("")
|
const [reason, setReason] = useState("")
|
||||||
const closeModal = use(ModalCloseContext)
|
const closeModal = use(ModalCloseContext)
|
||||||
const onConfirmWrapped = useEvent((evt: React.FormEvent) => {
|
const onConfirmWrapped = (evt: React.FormEvent) => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
closeModal()
|
closeModal()
|
||||||
onConfirm(reason)
|
onConfirm(reason)
|
||||||
})
|
}
|
||||||
const onChangeReason = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setReason(evt.target.value)
|
|
||||||
}, [])
|
|
||||||
return <form onSubmit={onConfirmWrapped}>
|
return <form onSubmit={onConfirmWrapped}>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
<div className="timeline-event-container">
|
<div className="timeline-event-container">
|
||||||
|
@ -49,7 +46,13 @@ const ConfirmWithMessageModal = ({
|
||||||
<div className="confirm-description">
|
<div className="confirm-description">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</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">
|
<div className="confirm-buttons">
|
||||||
<button type="button" onClick={closeModal}>Cancel</button>
|
<button type="button" onClick={closeModal}>Cancel</button>
|
||||||
<button type="submit">{confirmButton}</button>
|
<button type="submit">{confirmButton}</button>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 Client from "@/api/client.ts"
|
||||||
import { MemDBEvent } from "@/api/types"
|
import { MemDBEvent } from "@/api/types"
|
||||||
import { emojiToReactionContent } from "@/util/emoji"
|
import { emojiToReactionContent } from "@/util/emoji"
|
||||||
|
@ -43,11 +43,11 @@ export const usePrimaryItems = (
|
||||||
const closeModal = !isHover ? use(ModalCloseContext) : noop
|
const closeModal = !isHover ? use(ModalCloseContext) : noop
|
||||||
const openModal = use(ModalContext)
|
const openModal = use(ModalContext)
|
||||||
|
|
||||||
const onClickReply = useCallback(() => {
|
const onClickReply = () => {
|
||||||
roomCtx.setReplyTo(evt.event_id)
|
roomCtx.setReplyTo(evt.event_id)
|
||||||
closeModal()
|
closeModal()
|
||||||
}, [roomCtx, evt.event_id, closeModal])
|
}
|
||||||
const onClickReact = useCallback((mevt: React.MouseEvent<HTMLButtonElement>) => {
|
const onClickReact = (mevt: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
const emojiPickerHeight = 34 * 16
|
const emojiPickerHeight = 34 * 16
|
||||||
setForceOpen?.(true)
|
setForceOpen?.(true)
|
||||||
openModal({
|
openModal({
|
||||||
|
@ -63,20 +63,20 @@ export const usePrimaryItems = (
|
||||||
/>,
|
/>,
|
||||||
onClose: () => setForceOpen?.(false),
|
onClose: () => setForceOpen?.(false),
|
||||||
})
|
})
|
||||||
}, [client, roomCtx, evt, style, setForceOpen, openModal])
|
}
|
||||||
const onClickEdit = useCallback(() => {
|
const onClickEdit = () => {
|
||||||
closeModal()
|
closeModal()
|
||||||
roomCtx.setEditing(evt)
|
roomCtx.setEditing(evt)
|
||||||
}, [roomCtx, evt, closeModal])
|
}
|
||||||
const onClickResend = useCallback(() => {
|
const onClickResend = () => {
|
||||||
if (!evt.transaction_id) {
|
if (!evt.transaction_id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
closeModal()
|
closeModal()
|
||||||
client.resendEvent(evt.transaction_id)
|
client.resendEvent(evt.transaction_id)
|
||||||
.catch(err => window.alert(`Failed to resend message: ${err}`))
|
.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
|
const moreMenuHeight = 4 * 40
|
||||||
setForceOpen!(true)
|
setForceOpen!(true)
|
||||||
openModal({
|
openModal({
|
||||||
|
@ -87,7 +87,7 @@ export const usePrimaryItems = (
|
||||||
/>,
|
/>,
|
||||||
onClose: () => setForceOpen!(false),
|
onClose: () => setForceOpen!(false),
|
||||||
})
|
})
|
||||||
}, [evt, roomCtx, setForceOpen, openModal])
|
}
|
||||||
const isEditing = useEventAsState(roomCtx.isEditing)
|
const isEditing = useEventAsState(roomCtx.isEditing)
|
||||||
const [isPending, pendingTitle] = getPending(evt)
|
const [isPending, pendingTitle] = getPending(evt)
|
||||||
const isEncrypted = getEncryption(roomCtx.store)
|
const isEncrypted = getEncryption(roomCtx.store)
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 Client from "@/api/client.ts"
|
||||||
import { useRoomState } from "@/api/statestore"
|
import { useRoomState } from "@/api/statestore"
|
||||||
import { MemDBEvent } from "@/api/types"
|
import { MemDBEvent } from "@/api/types"
|
||||||
|
@ -35,14 +35,14 @@ export const useSecondaryItems = (
|
||||||
) => {
|
) => {
|
||||||
const closeModal = use(ModalCloseContext)
|
const closeModal = use(ModalCloseContext)
|
||||||
const openModal = use(ModalContext)
|
const openModal = use(ModalContext)
|
||||||
const onClickViewSource = useCallback(() => {
|
const onClickViewSource = () => {
|
||||||
openModal({
|
openModal({
|
||||||
dimmed: true,
|
dimmed: true,
|
||||||
boxed: true,
|
boxed: true,
|
||||||
content: <JSONView data={evt} />,
|
content: <JSONView data={evt} />,
|
||||||
})
|
})
|
||||||
}, [evt, openModal])
|
}
|
||||||
const onClickReport = useCallback(() => {
|
const onClickReport = () => {
|
||||||
openModal({
|
openModal({
|
||||||
dimmed: true,
|
dimmed: true,
|
||||||
boxed: true,
|
boxed: true,
|
||||||
|
@ -61,8 +61,8 @@ export const useSecondaryItems = (
|
||||||
/>
|
/>
|
||||||
</RoomContext>,
|
</RoomContext>,
|
||||||
})
|
})
|
||||||
}, [evt, roomCtx, openModal, client])
|
}
|
||||||
const onClickRedact = useCallback(() => {
|
const onClickRedact = () => {
|
||||||
openModal({
|
openModal({
|
||||||
dimmed: true,
|
dimmed: true,
|
||||||
boxed: true,
|
boxed: true,
|
||||||
|
@ -81,17 +81,12 @@ export const useSecondaryItems = (
|
||||||
/>
|
/>
|
||||||
</RoomContext>,
|
</RoomContext>,
|
||||||
})
|
})
|
||||||
}, [evt, roomCtx, openModal, client])
|
}
|
||||||
const onClickPin = useCallback(() => {
|
const onClickPin = (pin: boolean) => () => {
|
||||||
closeModal()
|
closeModal()
|
||||||
client.pinMessage(roomCtx.store, evt.event_id, true)
|
client.pinMessage(roomCtx.store, evt.event_id, pin)
|
||||||
.catch(err => window.alert(`Failed to pin message: ${err}`))
|
.catch(err => window.alert(`Failed to ${pin ? "pin" : "unpin"} 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])
|
|
||||||
|
|
||||||
const [isPending, pendingTitle] = getPending(evt)
|
const [isPending, pendingTitle] = getPending(evt)
|
||||||
useRoomState(roomCtx.store, "m.room.power_levels", "")
|
useRoomState(roomCtx.store, "m.room.power_levels", "")
|
||||||
|
@ -109,8 +104,12 @@ export const useSecondaryItems = (
|
||||||
return <>
|
return <>
|
||||||
<button onClick={onClickViewSource}><ViewSourceIcon/>View source</button>
|
<button onClick={onClickViewSource}><ViewSourceIcon/>View source</button>
|
||||||
{ownPL >= pinPL && (pins.includes(evt.event_id)
|
{ownPL >= pinPL && (pins.includes(evt.event_id)
|
||||||
? <button onClick={onClickUnpin}><UnpinIcon/>Unpin message</button>
|
? <button onClick={onClickPin(false)}>
|
||||||
: <button onClick={onClickPin} title={pendingTitle} disabled={isPending}><PinIcon/>Pin message</button>)}
|
<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>
|
<button onClick={onClickReport} disabled={isPending} title={pendingTitle}><ReportIcon/>Report</button>
|
||||||
{canRedact && <button
|
{canRedact && <button
|
||||||
onClick={onClickRedact}
|
onClick={onClickRedact}
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { CSSProperties } from "react"
|
import React, { CSSProperties } from "react"
|
||||||
import useEvent from "@/util/useEvent.ts"
|
|
||||||
import "./ResizeHandle.css"
|
import "./ResizeHandle.css"
|
||||||
|
|
||||||
export interface ResizeHandleProps {
|
export interface ResizeHandleProps {
|
||||||
|
@ -28,7 +27,7 @@ export interface ResizeHandleProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResizeHandle = ({ width, minWidth, maxWidth, setWidth, style, className, inverted }: 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 origWidth = width
|
||||||
const startPos = evt.clientX
|
const startPos = evt.clientX
|
||||||
const onMouseMove = (evt: MouseEvent) => {
|
const onMouseMove = (evt: MouseEvent) => {
|
||||||
|
@ -46,7 +45,7 @@ const ResizeHandle = ({ width, minWidth, maxWidth, setWidth, style, className, i
|
||||||
document.addEventListener("mousemove", onMouseMove)
|
document.addEventListener("mousemove", onMouseMove)
|
||||||
document.addEventListener("mouseup", onMouseUp)
|
document.addEventListener("mouseup", onMouseUp)
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
})
|
}
|
||||||
return <div className={`resize-handle-outer ${className ?? ""}`} style={style}>
|
return <div className={`resize-handle-outer ${className ?? ""}`} style={style}>
|
||||||
<div className="resize-handle-inner" onMouseDown={onMouseDown}/>
|
<div className="resize-handle-inner" onMouseDown={onMouseDown}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue