mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web/composer: allow switching edit target with arrow keys
This commit is contained in:
parent
cc0067bb3f
commit
f88b1b5b7f
4 changed files with 46 additions and 16 deletions
|
@ -89,32 +89,30 @@ const MessageComposer = () => {
|
||||||
roomCtx.setEditing = useCallback((evt: MemDBEvent | null) => {
|
roomCtx.setEditing = useCallback((evt: MemDBEvent | null) => {
|
||||||
if (evt === null) {
|
if (evt === null) {
|
||||||
rawSetEditing(null)
|
rawSetEditing(null)
|
||||||
setState(emptyComposer)
|
setState(draftStore.get(room.roomID) ?? emptyComposer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (state.text || state.media) {
|
|
||||||
// TODO save as draft instead of discarding?
|
|
||||||
const ok = window.confirm("Discard current message to start editing?")
|
|
||||||
if (!ok) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const evtContent = evt.content as MessageEventContent
|
const evtContent = evt.content as MessageEventContent
|
||||||
const mediaMsgTypes = ["m.image", "m.audio", "m.video", "m.file"]
|
const mediaMsgTypes = ["m.image", "m.audio", "m.video", "m.file"]
|
||||||
const isMedia = mediaMsgTypes.includes(evtContent.msgtype) && (evt.content?.url || evt.content?.file?.url)
|
const isMedia = mediaMsgTypes.includes(evtContent.msgtype)
|
||||||
|
&& Boolean(evt.content?.url || evt.content?.file?.url)
|
||||||
rawSetEditing(evt)
|
rawSetEditing(evt)
|
||||||
setState({
|
setState({
|
||||||
media: isMedia ? evtContent as MediaMessageEventContent : null,
|
media: isMedia ? evtContent as MediaMessageEventContent : null,
|
||||||
text: (!evt.content.filename || evt.content.filename !== evt.content.body) ? (evtContent.body ?? "") : "",
|
text: (!evt.content.filename || evt.content.filename !== evt.content.body) ? (evtContent.body ?? "") : "",
|
||||||
})
|
})
|
||||||
textInput.current?.focus()
|
textInput.current?.focus()
|
||||||
}, [state])
|
}, [room.roomID])
|
||||||
const sendMessage = useEvent((evt: React.FormEvent) => {
|
const sendMessage = useEvent((evt: React.FormEvent) => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
if (state.text === "" && !state.media) {
|
if (state.text === "" && !state.media) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setState(emptyComposer)
|
if (editing) {
|
||||||
|
setState(draftStore.get(room.roomID) ?? emptyComposer)
|
||||||
|
} else {
|
||||||
|
setState(emptyComposer)
|
||||||
|
}
|
||||||
rawSetEditing(null)
|
rawSetEditing(null)
|
||||||
setAutocomplete(null)
|
setAutocomplete(null)
|
||||||
const mentions: Mentions = {
|
const mentions: Mentions = {
|
||||||
|
@ -184,8 +182,7 @@ const MessageComposer = () => {
|
||||||
const onComposerKeyDown = useEvent((evt: React.KeyboardEvent) => {
|
const onComposerKeyDown = useEvent((evt: React.KeyboardEvent) => {
|
||||||
if (evt.key === "Enter" && !evt.shiftKey) {
|
if (evt.key === "Enter" && !evt.shiftKey) {
|
||||||
sendMessage(evt)
|
sendMessage(evt)
|
||||||
}
|
} else if (autocomplete && !evt.ctrlKey && !evt.altKey) {
|
||||||
if (autocomplete && !evt.ctrlKey && !evt.altKey) {
|
|
||||||
if (!evt.shiftKey && (evt.key === "Tab" || evt.key === "ArrowDown")) {
|
if (!evt.shiftKey && (evt.key === "Tab" || evt.key === "ArrowDown")) {
|
||||||
setAutocomplete({ ...autocomplete, selected: (autocomplete.selected ?? -1) + 1 })
|
setAutocomplete({ ...autocomplete, selected: (autocomplete.selected ?? -1) + 1 })
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
|
@ -193,6 +190,30 @@ const MessageComposer = () => {
|
||||||
setAutocomplete({ ...autocomplete, selected: (autocomplete.selected ?? 0) - 1 })
|
setAutocomplete({ ...autocomplete, selected: (autocomplete.selected ?? 0) - 1 })
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
}
|
}
|
||||||
|
} else if (!autocomplete && textInput.current) {
|
||||||
|
const inp = textInput.current
|
||||||
|
if (evt.key === "ArrowUp" && inp.selectionStart === 0 && inp.selectionEnd === 0) {
|
||||||
|
const currentlyEditing = editing
|
||||||
|
? roomCtx.ownMessages.indexOf(editing.rowid)
|
||||||
|
: roomCtx.ownMessages.length
|
||||||
|
const prevEventToEditID = roomCtx.ownMessages[currentlyEditing - 1]
|
||||||
|
const prevEventToEdit = prevEventToEditID ? room.eventsByRowID.get(prevEventToEditID) : undefined
|
||||||
|
if (prevEventToEdit) {
|
||||||
|
roomCtx.setEditing(prevEventToEdit)
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
} else if (editing && evt.key === "ArrowDown" && inp.selectionStart === state.text.length) {
|
||||||
|
const currentlyEditingIdx = roomCtx.ownMessages.indexOf(editing.rowid)
|
||||||
|
const nextEventToEdit = currentlyEditingIdx
|
||||||
|
? room.eventsByRowID.get(roomCtx.ownMessages[currentlyEditingIdx + 1]) : undefined
|
||||||
|
roomCtx.setEditing(nextEventToEdit ?? null)
|
||||||
|
// This timeout is very hacky and probably doesn't work in every case
|
||||||
|
setTimeout(() => inp.setSelectionRange(0, 0), 0)
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (editing && evt.key === "Escape") {
|
||||||
|
roomCtx.setEditing(null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const onChange = useEvent((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const onChange = useEvent((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
|
|
@ -15,7 +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 { RefObject, createContext, createRef, use } from "react"
|
import { RefObject, createContext, createRef, use } from "react"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomStateStore } from "@/api/statestore"
|
||||||
import { EventID, MemDBEvent } from "@/api/types"
|
import { EventID, EventRowID, MemDBEvent } from "@/api/types"
|
||||||
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
||||||
|
|
||||||
const noop = (name: string) => () => {
|
const noop = (name: string) => () => {
|
||||||
|
@ -27,6 +27,7 @@ export class RoomContextData {
|
||||||
public setReplyTo: (eventID: EventID | null) => void = noop("setReplyTo")
|
public setReplyTo: (eventID: EventID | null) => void = noop("setReplyTo")
|
||||||
public setEditing: (evt: MemDBEvent | null) => void = noop("setEditing")
|
public setEditing: (evt: MemDBEvent | null) => void = noop("setEditing")
|
||||||
public isEditing = new NonNullCachedEventDispatcher<boolean>(false)
|
public isEditing = new NonNullCachedEventDispatcher<boolean>(false)
|
||||||
|
public ownMessages: EventRowID[] = []
|
||||||
|
|
||||||
constructor(public store: RoomStateStore) {}
|
constructor(public store: RoomStateStore) {}
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,8 @@ const EventMenu = ({ evt }: EventHoverMenuProps) => {
|
||||||
title={isEditing ? "Can't reply to messages while editing a message" : undefined}
|
title={isEditing ? "Can't reply to messages while editing a message" : undefined}
|
||||||
onClick={onClickReply}
|
onClick={onClickReply}
|
||||||
><ReplyIcon/></button>
|
><ReplyIcon/></button>
|
||||||
{evt.sender === userID && evt.type === "m.room.message" && <button onClick={onClickEdit}><EditIcon/></button>}
|
{evt.sender === userID && evt.type === "m.room.message" && evt.relation_type !== "m.replace"
|
||||||
|
&& <button onClick={onClickEdit}><EditIcon/></button>}
|
||||||
{ownPL >= pinPL && (pins.includes(evt.event_id)
|
{ownPL >= pinPL && (pins.includes(evt.event_id)
|
||||||
? <button onClick={onClickUnpin}><UnpinIcon/></button>
|
? <button onClick={onClickUnpin}><UnpinIcon/></button>
|
||||||
: <button onClick={onClickPin}><PinIcon/></button>)}
|
: <button onClick={onClickPin}><PinIcon/></button>)}
|
||||||
|
|
|
@ -53,6 +53,7 @@ const TimelineView = () => {
|
||||||
oldScrollHeight.current = timelineViewRef.current.scrollHeight
|
oldScrollHeight.current = timelineViewRef.current.scrollHeight
|
||||||
}
|
}
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
const bottomRef = roomCtx.timelineBottomRef
|
||||||
if (bottomRef.current && scrolledToBottom.current) {
|
if (bottomRef.current && scrolledToBottom.current) {
|
||||||
// For any timeline changes, if we were at the bottom, scroll to the new bottom
|
// For any timeline changes, if we were at the bottom, scroll to the new bottom
|
||||||
bottomRef.current.scrollIntoView()
|
bottomRef.current.scrollIntoView()
|
||||||
|
@ -61,7 +62,13 @@ const TimelineView = () => {
|
||||||
timelineViewRef.current.scrollTop += timelineViewRef.current.scrollHeight - oldScrollHeight.current
|
timelineViewRef.current.scrollTop += timelineViewRef.current.scrollHeight - oldScrollHeight.current
|
||||||
}
|
}
|
||||||
prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0
|
prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0
|
||||||
}, [bottomRef, timeline])
|
roomCtx.ownMessages = timeline
|
||||||
|
.filter(evt => evt !== null
|
||||||
|
&& evt.sender === client.userID
|
||||||
|
&& evt.type === "m.room.message"
|
||||||
|
&& evt.relation_type !== "m.replace")
|
||||||
|
.map(evt => evt!.rowid)
|
||||||
|
}, [client.userID, roomCtx, timeline])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newestEvent = timeline[timeline.length - 1]
|
const newestEvent = timeline[timeline.length - 1]
|
||||||
if (
|
if (
|
||||||
|
|
Loading…
Add table
Reference in a new issue