web/timeline: mark thread replies

This commit is contained in:
Tulir Asokan 2024-10-22 20:00:15 +03:00
parent c4266fbc22
commit 082e5642aa
4 changed files with 24 additions and 7 deletions

View file

@ -255,7 +255,12 @@ const MessageComposer = ({ room, scrollToBottomRef, setReplyToRef }: MessageComp
setState={setState} setState={setState}
setAutocomplete={setAutocomplete} setAutocomplete={setAutocomplete}
/></div>} /></div>}
{replyToEvt && <ReplyBody room={room} event={replyToEvt} onClose={closeReply}/>} {replyToEvt && <ReplyBody
room={room}
event={replyToEvt}
onClose={closeReply}
isThread={replyToEvt.content["m.relates_to"]?.rel_type === "m.thread"}
/>}
{loadingMedia && <div className="composer-media"><ScaleLoader/></div>} {loadingMedia && <div className="composer-media"><ScaleLoader/></div>}
{state.media && <ComposerMedia content={state.media} clearMedia={clearMedia}/>} {state.media && <ComposerMedia content={state.media} clearMedia={clearMedia}/>}
<div className="input-area"> <div className="input-area">

View file

@ -27,6 +27,12 @@ blockquote.reply-body {
color: #666; color: #666;
} }
&.thread > div.reply-sender > span.event-sender::after {
content: " (thread)";
font-size: .75rem;
color: #666;
}
> div.reply-sender { > div.reply-sender {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -25,15 +25,17 @@ import "./ReplyBody.css"
interface ReplyBodyProps { interface ReplyBodyProps {
room: RoomStateStore room: RoomStateStore
event: MemDBEvent event: MemDBEvent
isThread: boolean
onClose?: (evt: React.MouseEvent) => void onClose?: (evt: React.MouseEvent) => void
} }
interface ReplyIDBodyProps { interface ReplyIDBodyProps {
room: RoomStateStore room: RoomStateStore
eventID: EventID eventID: EventID
isThread: boolean
} }
export const ReplyIDBody = ({ room, eventID }: ReplyIDBodyProps) => { export const ReplyIDBody = ({ room, eventID, isThread }: ReplyIDBodyProps) => {
const event = useRoomEvent(room, eventID) const event = useRoomEvent(room, eventID)
if (!event) { if (!event) {
// This caches whether the event is requested or not, so it doesn't need to be wrapped in an effect. // This caches whether the event is requested or not, so it doesn't need to be wrapped in an effect.
@ -42,7 +44,7 @@ export const ReplyIDBody = ({ room, eventID }: ReplyIDBodyProps) => {
Reply to unknown event<br/><code>{eventID}</code> Reply to unknown event<br/><code>{eventID}</code>
</blockquote> </blockquote>
} }
return <ReplyBody room={room} event={event}/> return <ReplyBody room={room} event={event} isThread={isThread}/>
} }
const onClickReply = (evt: React.MouseEvent) => { const onClickReply = (evt: React.MouseEvent) => {
@ -62,13 +64,13 @@ const onClickReply = (evt: React.MouseEvent) => {
} }
} }
export const ReplyBody = ({ room, event, onClose }: ReplyBodyProps) => { export const ReplyBody = ({ room, event, onClose, isThread }: ReplyBodyProps) => {
const memberEvt = useRoomState(room, "m.room.member", event.sender) const memberEvt = useRoomState(room, "m.room.member", event.sender)
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
const BodyType = getBodyType(event, true) const BodyType = getBodyType(event, true)
return <blockquote return <blockquote
data-reply-to={event.event_id} data-reply-to={event.event_id}
className={`reply-body ${onClose ? "composer" : ""}`} className={`reply-body ${onClose ? "composer" : ""} ${isThread ? "thread" : ""}`}
onClick={onClickReply} onClick={onClickReply}
> >
<div className="reply-sender"> <div className="reply-sender">

View file

@ -92,7 +92,7 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyToRef }: TimelineEventProps
const fullTime = fullTimeFormatter.format(eventTS) const fullTime = fullTimeFormatter.format(eventTS)
const shortTime = formatShortTime(eventTS) const shortTime = formatShortTime(eventTS)
const editTime = editEventTS ? `Edited at ${fullTimeFormatter.format(editEventTS)}` : null const editTime = editEventTS ? `Edited at ${fullTimeFormatter.format(editEventTS)}` : null
const replyTo = (evt.orig_content ?? evt.content)["m.relates_to"]?.["m.in_reply_to"]?.event_id const replyTo = evt.content["m.relates_to"]?.["m.in_reply_to"]?.event_id
const mainEvent = <div data-event-id={evt.event_id} className={wrapperClassNames.join(" ")}> const mainEvent = <div data-event-id={evt.event_id} className={wrapperClassNames.join(" ")}>
<div className="sender-avatar" title={evt.sender}> <div className="sender-avatar" title={evt.sender}>
<img <img
@ -114,7 +114,11 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyToRef }: TimelineEventProps
<span className="event-time" title={editTime ? `${fullTime} - ${editTime}` : fullTime}>{shortTime}</span> <span className="event-time" title={editTime ? `${fullTime} - ${editTime}` : fullTime}>{shortTime}</span>
</div> </div>
<div className="event-content"> <div className="event-content">
{isEventID(replyTo) && BodyType !== HiddenEvent ? <ReplyIDBody room={room} eventID={replyTo}/> : null} {isEventID(replyTo) && BodyType !== HiddenEvent ? <ReplyIDBody
room={room}
eventID={replyTo}
isThread={evt.content["m.relates_to"]?.rel_type === "m.thread"}
/> : null}
<ContentErrorBoundary> <ContentErrorBoundary>
<BodyType room={room} sender={memberEvt} event={evt}/> <BodyType room={room} sender={memberEvt} event={evt}/>
</ContentErrorBoundary> </ContentErrorBoundary>