forked from Mirrors/gomuks
web/timeline: omit profile on consecutive messages from same sender
This commit is contained in:
parent
cba5dbd912
commit
3ded0f4eb9
3 changed files with 61 additions and 12 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue