web/composer: add option to start new thread when replying

Fixes #594
This commit is contained in:
Tulir Asokan 2025-02-24 00:08:32 +02:00
parent 548c8a9a94
commit 5d41b49462
2 changed files with 34 additions and 2 deletions

View file

@ -55,6 +55,7 @@ export interface ComposerState {
replyTo: EventID | null
silentReply: boolean
explicitReplyInThread: boolean
startNewThread: boolean
uninited?: boolean
}
@ -67,6 +68,7 @@ const emptyComposer: ComposerState = {
location: null,
silentReply: false,
explicitReplyInThread: false,
startNewThread: false,
}
const uninitedComposer: ComposerState = { ...emptyComposer, uninited: true }
const composerReducer = (
@ -116,7 +118,7 @@ const MessageComposer = () => {
document.execCommand("insertText", false, text)
}, [])
roomCtx.setReplyTo = useCallback((evt: EventID | null) => {
setState({ replyTo: evt, silentReply: false, explicitReplyInThread: false })
setState({ replyTo: evt, silentReply: false, explicitReplyInThread: false, startNewThread: false })
textInput.current?.focus()
}, [])
const setSilentReply = useCallback((newVal: boolean | React.MouseEvent) => {
@ -135,6 +137,14 @@ const MessageComposer = () => {
setState(state => ({ explicitReplyInThread: !state.explicitReplyInThread }))
}
}, [])
const setStartNewThread = useCallback((newVal: boolean | React.MouseEvent) => {
if (typeof newVal === "boolean") {
setState({ startNewThread: newVal })
} else {
newVal.stopPropagation()
setState(state => ({ startNewThread: !state.startNewThread }))
}
}, [])
roomCtx.setEditing = useCallback((evt: MemDBEvent | null) => {
if (evt === null) {
rawSetEditing(null)
@ -160,6 +170,7 @@ const MessageComposer = () => {
replyTo: null,
silentReply: false,
explicitReplyInThread: false,
startNewThread: false,
})
textInput.current?.focus()
}, [room.roomID])
@ -204,6 +215,10 @@ const MessageComposer = () => {
relates_to.rel_type = "m.thread"
relates_to.event_id = replyToEvt.content?.["m.relates_to"].event_id
relates_to.is_falling_back = !state.explicitReplyInThread
} else if (state.startNewThread) {
relates_to.rel_type = "m.thread"
relates_to.event_id = replyToEvt.event_id
relates_to.is_falling_back = true
}
}
let base_content: MessageEventContent | undefined
@ -580,6 +595,8 @@ const MessageComposer = () => {
onSetSilent={setSilentReply}
isExplicitInThread={state.explicitReplyInThread}
onSetExplicitInThread={setExplicitReplyInThread}
startNewThread={state.startNewThread}
onSetStartNewThread={setStartNewThread}
/>}
{editing && <ReplyBody
room={room}

View file

@ -39,6 +39,8 @@ interface ReplyBodyProps {
onSetSilent?: (evt: React.MouseEvent) => void
isExplicitInThread?: boolean
onSetExplicitInThread?: (evt: React.MouseEvent) => void
startNewThread?: boolean
onSetStartNewThread?: (evt: React.MouseEvent) => void
}
interface ReplyIDBodyProps {
@ -83,7 +85,10 @@ const onClickReply = (evt: React.MouseEvent) => {
}
export const ReplyBody = ({
room, event, onClose, isThread, isEditing, isSilent, onSetSilent, isExplicitInThread, onSetExplicitInThread, small,
room, event, onClose, isThread, isEditing, small,
isSilent, onSetSilent,
isExplicitInThread, onSetExplicitInThread,
startNewThread, onSetStartNewThread,
}: ReplyBodyProps) => {
const client = use(ClientContext)
const memberEvt = useRoomMember(client, room, event.sender)
@ -164,6 +169,16 @@ export const ReplyBody = ({
>
{isExplicitInThread ? <ReplyIcon /> : <ThreadIcon />}
</TooltipButton>}
{!isThread && onSetStartNewThread && <TooltipButton
tooltipText={startNewThread
? "Click to reply in main timeline instead of starting a new thread"
: "Click to start a new thread instead of replying"}
tooltipDirection="left"
className="thread-explicit-reply"
onClick={onSetStartNewThread}
>
{startNewThread ? <ThreadIcon /> : <ReplyIcon />}
</TooltipButton>}
{onClose && <button className="close-reply" onClick={onClose}><CloseIcon/></button>}
</div>}
</div>