web/timeline: omit profile on consecutive messages from same sender

This commit is contained in:
Tulir Asokan 2024-10-12 13:41:15 +03:00
parent cba5dbd912
commit 3ded0f4eb9
3 changed files with 61 additions and 12 deletions

View file

@ -33,15 +33,40 @@ div.timeline-event {
} }
} }
> div.event-time-only {
grid-area: timestamp;
display: none;
align-items: end;
justify-content: center;
font-size: .8rem;
max-height: 1.25rem;
margin-right: .25rem;
}
> div.event-content { > div.event-content {
grid-area: content; grid-area: content;
overflow: hidden; overflow: hidden;
} }
&.same-sender {
grid-template:
"timestamp content" auto
/ 2.75rem 1fr;
margin-top: 0;
> div.sender-avatar, > div.event-sender-and-time {
display: none;
}
> div.event-time-only {
display: flex;
}
}
&.hidden-event { &.hidden-event {
grid-template: grid-template:
"sender avatar content" auto "timestamp avatar content" auto
/ 2.5rem 1.5rem 1fr; / 2.75rem 1.5rem 1fr;
margin-top: 0; margin-top: 0;
> div.sender-avatar { > div.sender-avatar {
@ -58,9 +83,11 @@ div.timeline-event {
} }
> div.event-sender-and-time { > div.event-sender-and-time {
> span.event-sender, > span.event-edited { display: none;
display: none; }
}
> div.event-time-only {
display: flex;
} }
} }
} }

View file

@ -27,6 +27,7 @@ import "./TimelineEvent.css"
export interface TimelineEventProps { export interface TimelineEventProps {
room: RoomStateStore room: RoomStateStore
evt: MemDBEvent evt: MemDBEvent
prevEvt: MemDBEvent | null
} }
function getBodyType(evt: MemDBEvent): React.FunctionComponent<EventContentProps> { function getBodyType(evt: MemDBEvent): React.FunctionComponent<EventContentProps> {
@ -61,13 +62,22 @@ const EventReactions = ({ reactions }: { reactions: Record<string, number> }) =>
</div> </div>
} }
const TimelineEvent = ({ room, evt }: TimelineEventProps) => { const TimelineEvent = ({ room, evt, prevEvt }: TimelineEventProps) => {
const memberEvt = room.getStateEvent("m.room.member", evt.sender) const memberEvt = room.getStateEvent("m.room.member", evt.sender)
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
const BodyType = getBodyType(evt) const BodyType = getBodyType(evt)
const eventTS = new Date(evt.timestamp) const eventTS = new Date(evt.timestamp)
const editEventTS = evt.last_edit ? new Date(evt.last_edit.timestamp) : null const editEventTS = evt.last_edit ? new Date(evt.last_edit.timestamp) : null
return <div className={`timeline-event ${BodyType === HiddenEvent ? "hidden-event" : ""}`}> const wrapperClassNames = ["timeline-event"]
if (BodyType === HiddenEvent) {
wrapperClassNames.push("hidden-event")
} else if (prevEvt?.sender === evt.sender && getBodyType(prevEvt) !== HiddenEvent) {
wrapperClassNames.push("same-sender")
}
const fullTime = fullTimeFormatter.format(eventTS)
const shortTime = formatShortTime(eventTS)
const editTime = editEventTS ? `Edited at ${fullTimeFormatter.format(editEventTS)}` : null
return <div className={wrapperClassNames.join(" ")}>
<div className="sender-avatar" title={evt.sender}> <div className="sender-avatar" title={evt.sender}>
<img <img
className="avatar" className="avatar"
@ -78,11 +88,14 @@ const TimelineEvent = ({ room, evt }: TimelineEventProps) => {
</div> </div>
<div className="event-sender-and-time"> <div className="event-sender-and-time">
<span className="event-sender">{memberEvtContent?.displayname ?? evt.sender}</span> <span className="event-sender">{memberEvtContent?.displayname ?? evt.sender}</span>
<span className="event-time" title={fullTimeFormatter.format(eventTS)}>{formatShortTime(eventTS)}</span> <span className="event-time" title={fullTime}>{shortTime}</span>
{editEventTS ? <span className="event-edited" title={`Edited at ${fullTimeFormatter.format(editEventTS)}`}> {(editEventTS && editTime) ? <span className="event-edited" title={editTime}>
(edited at {formatShortTime(editEventTS)}) (edited at {formatShortTime(editEventTS)})
</span> : null} </span> : null}
</div> </div>
<div className="event-time-only">
<span className="event-time" title={editTime ? `${fullTime} - ${editTime}` : fullTime}>{shortTime}</span>
</div>
<div className="event-content"> <div className="event-content">
<BodyType room={room} event={evt}/> <BodyType room={room} event={evt}/>
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null} {evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}

View file

@ -15,6 +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 { use, useCallback, useEffect, useRef } from "react" import { use, useCallback, useEffect, useRef } from "react"
import { RoomStateStore, useRoomTimeline } from "../../api/statestore.ts" import { RoomStateStore, useRoomTimeline } from "../../api/statestore.ts"
import { MemDBEvent } from "../../api/types"
import { ClientContext } from "../ClientContext.ts" import { ClientContext } from "../ClientContext.ts"
import TimelineEvent from "./TimelineEvent.tsx" import TimelineEvent from "./TimelineEvent.tsx"
import "./TimelineView.css" import "./TimelineView.css"
@ -80,15 +81,23 @@ const TimelineView = ({ room }: TimelineViewProps) => {
return () => observer.unobserve(topElem) return () => observer.unobserve(topElem)
}, [loadHistory, topRef]) }, [loadHistory, topRef])
let prevEvt: MemDBEvent | null = null
return <div className="timeline-view" onScroll={handleScroll} ref={timelineViewRef}> return <div className="timeline-view" onScroll={handleScroll} ref={timelineViewRef}>
<div className="timeline-beginning"> <div className="timeline-beginning">
<button onClick={loadHistory}>Load history</button> <button onClick={loadHistory}>Load history</button>
</div> </div>
<div className="timeline-list"> <div className="timeline-list">
<div className="timeline-top-ref" ref={topRef}/> <div className="timeline-top-ref" ref={topRef}/>
{timeline.map(entry => entry ? <TimelineEvent {timeline.map(entry => {
key={entry.rowid} room={room} evt={entry} if (!entry) {
/> : null)} return null
}
const thisEvt = <TimelineEvent
key={entry.rowid} room={room} evt={entry} prevEvt={prevEvt}
/>
prevEvt = entry
return thisEvt
})}
<div className="timeline-bottom-ref" ref={bottomRef}/> <div className="timeline-bottom-ref" ref={bottomRef}/>
</div> </div>
</div> </div>