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

View file

@ -39,6 +39,8 @@ interface ReplyBodyProps {
onSetSilent?: (evt: React.MouseEvent) => void onSetSilent?: (evt: React.MouseEvent) => void
isExplicitInThread?: boolean isExplicitInThread?: boolean
onSetExplicitInThread?: (evt: React.MouseEvent) => void onSetExplicitInThread?: (evt: React.MouseEvent) => void
startNewThread?: boolean
onSetStartNewThread?: (evt: React.MouseEvent) => void
} }
interface ReplyIDBodyProps { interface ReplyIDBodyProps {
@ -83,7 +85,10 @@ const onClickReply = (evt: React.MouseEvent) => {
} }
export const ReplyBody = ({ 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) => { }: ReplyBodyProps) => {
const client = use(ClientContext) const client = use(ClientContext)
const memberEvt = useRoomMember(client, room, event.sender) const memberEvt = useRoomMember(client, room, event.sender)
@ -164,6 +169,16 @@ export const ReplyBody = ({
> >
{isExplicitInThread ? <ReplyIcon /> : <ThreadIcon />} {isExplicitInThread ? <ReplyIcon /> : <ThreadIcon />}
</TooltipButton>} </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>} {onClose && <button className="close-reply" onClick={onClose}><CloseIcon/></button>}
</div>} </div>}
</div> </div>