Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 5ff9651

Browse files
authored
Render timeline separator for late event groups (#11739)
* Use Compound tooltips on MessageTimestamp to improve UX of date time discovery Signed-off-by: Michael Telatynski <[email protected]> * Show io.element.late_event in MessageTimestamp when known Signed-off-by: Michael Telatynski <[email protected]> * Update snapshot Signed-off-by: Michael Telatynski <[email protected]> * Avoid needing new Compound changes Signed-off-by: Michael Telatynski <[email protected]> * Move groupers into their own directory Signed-off-by: Michael Telatynski <[email protected]> * Refactor date separator code to be more generic Signed-off-by: Michael Telatynski <[email protected]> * Render timeline separator for late event groups Signed-off-by: Michael Telatynski <[email protected]> * Fix date used in copy Signed-off-by: Michael Telatynski <[email protected]> * Update snapshot Signed-off-by: Michael Telatynski <[email protected]> * Move groupers into their own directory Signed-off-by: Michael Telatynski <[email protected]> * Update copy Signed-off-by: Michael Telatynski <[email protected]> * Update copy Signed-off-by: Michael Telatynski <[email protected]> * Update snapshot Signed-off-by: Michael Telatynski <[email protected]> * i18n Signed-off-by: Michael Telatynski <[email protected]> * Add comments Signed-off-by: Michael Telatynski <[email protected]> * Add comments Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent dfdb613 commit 5ff9651

File tree

18 files changed

+199
-67
lines changed

18 files changed

+199
-67
lines changed

cypress/e2e/editing/editing.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ describe("Editing", () => {
117117
cy.get(".mx_EventTile").should("have.css", "padding-block-start", "0px");
118118

119119
// Assert that the date separator is rendered at the top
120-
cy.get("li:nth-child(1) .mx_DateSeparator").within(() => {
120+
cy.get("li:nth-child(1) .mx_TimelineSeparator").within(() => {
121121
cy.get("h2").within(() => {
122122
cy.findByText("today").should("have.css", "text-transform", "capitalize");
123123
});
@@ -182,7 +182,7 @@ describe("Editing", () => {
182182
// Assert that the message edit history dialog is rendered again
183183
cy.get(".mx_MessageEditHistoryDialog").within(() => {
184184
// Assert that the date is rendered
185-
cy.get("li:nth-child(1) .mx_DateSeparator").within(() => {
185+
cy.get("li:nth-child(1) .mx_TimelineSeparator").within(() => {
186186
cy.get("h2").within(() => {
187187
cy.findByText("today").should("have.css", "text-transform", "capitalize");
188188
});

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@
251251
@import "./views/messages/_RedactedBody.pcss";
252252
@import "./views/messages/_RoomAvatarEvent.pcss";
253253
@import "./views/messages/_TextualEvent.pcss";
254+
@import "./views/messages/_TimelineSeparator.pcss";
254255
@import "./views/messages/_UnknownBody.pcss";
255256
@import "./views/messages/_ViewSourceEvent.pcss";
256257
@import "./views/messages/_common_CryptoEvent.pcss";

res/css/views/messages/_DateSeparator.pcss

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,6 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
.mx_DateSeparator {
18-
clear: both;
19-
margin: 4px 0;
20-
display: flex;
21-
align-items: center;
22-
font: var(--cpd-font-body-md-regular);
23-
color: $roomtopic-color;
24-
}
25-
26-
.mx_DateSeparator > hr {
27-
flex: 1 1 0;
28-
height: 0;
29-
border: none;
30-
border-bottom: 1px solid $menu-selected-color;
31-
}
32-
3317
.mx_DateSeparator_dateContent {
3418
padding: 0 25px;
3519
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Copyright 2017 Vector Creations Ltd
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_TimelineSeparator {
18+
clear: both;
19+
margin: 4px 0;
20+
display: flex;
21+
align-items: center;
22+
font: var(--cpd-font-body-md-regular);
23+
color: $roomtopic-color;
24+
}
25+
26+
.mx_TimelineSeparator > hr {
27+
flex: 1 1 0;
28+
height: 0;
29+
border: none;
30+
border-bottom: 1px solid $menu-selected-color;
31+
}

res/css/views/right_panel/_ThreadPanel.pcss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ limitations under the License.
112112
/* Account for scrollbar when hovering */
113113
padding-top: 0;
114114

115-
.mx_DateSeparator {
115+
.mx_TimelineSeparator {
116116
display: none;
117117
}
118118

src/components/structures/MessagePanel.tsx

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ import classNames from "classnames";
2020
import { Room, MatrixClient, RoomStateEvent, EventStatus, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
2121
import { logger } from "matrix-js-sdk/src/logger";
2222
import { isSupportedReceiptType } from "matrix-js-sdk/src/utils";
23-
import { Optional } from "matrix-events-sdk";
2423

2524
import shouldHideEvent from "../../shouldHideEvent";
26-
import { wantsDateSeparator } from "../../DateUtils";
25+
import { formatDate, wantsDateSeparator } from "../../DateUtils";
2726
import { MatrixClientPeg } from "../../MatrixClientPeg";
2827
import SettingsStore from "../../settings/SettingsStore";
2928
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
@@ -40,6 +39,7 @@ import LegacyCallEventGrouper from "./LegacyCallEventGrouper";
4039
import WhoIsTypingTile from "../views/rooms/WhoIsTypingTile";
4140
import ScrollPanel, { IScrollState } from "./ScrollPanel";
4241
import DateSeparator from "../views/messages/DateSeparator";
42+
import TimelineSeparator, { SeparatorKind } from "../views/messages/TimelineSeparator";
4343
import ErrorBoundary from "../views/elements/ErrorBoundary";
4444
import ResizeNotifier from "../../utils/ResizeNotifier";
4545
import Spinner from "../views/elements/Spinner";
@@ -54,6 +54,8 @@ import { hasThreadSummary } from "../../utils/EventUtils";
5454
import { BaseGrouper } from "./grouper/BaseGrouper";
5555
import { MainGrouper } from "./grouper/MainGrouper";
5656
import { CreationGrouper } from "./grouper/CreationGrouper";
57+
import { _t } from "../../languageHandler";
58+
import { getLateEventInfo } from "./grouper/LateEventGrouper";
5759

5860
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
5961
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
@@ -739,39 +741,46 @@ export default class MessagePanel extends React.Component<IProps, IState> {
739741

740742
const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId();
741743
// local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators.
742-
let ts1 = mxEv.getTs();
743-
let eventDate = mxEv.getDate();
744-
if (mxEv.status) {
745-
eventDate = new Date();
746-
ts1 = eventDate.getTime();
747-
}
748-
749-
// do we need a date separator since the last event?
750-
const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate);
751-
if (wantsDateSeparator && !isGrouped && this.props.room) {
752-
const dateSeparator = (
753-
<li key={ts1}>
754-
<DateSeparator key={ts1} roomId={this.props.room.roomId} ts={ts1} />
755-
</li>
756-
);
757-
ret.push(dateSeparator);
744+
const ts1 = mxEv.getTs() ?? Date.now();
745+
746+
// do we need a separator since the last event?
747+
const wantsSeparator = this.wantsSeparator(prevEvent, mxEv);
748+
if (!isGrouped && this.props.room) {
749+
if (wantsSeparator === SeparatorKind.Date) {
750+
ret.push(
751+
<li key={ts1}>
752+
<DateSeparator key={ts1} roomId={this.props.room.roomId} ts={ts1} />
753+
</li>,
754+
);
755+
} else if (wantsSeparator === SeparatorKind.LateEvent) {
756+
const text = _t("timeline|late_event_separator", {
757+
dateTime: formatDate(mxEv.getDate() ?? new Date()),
758+
});
759+
ret.push(
760+
<li key={ts1}>
761+
<TimelineSeparator key={ts1} label={text}>
762+
{text}
763+
</TimelineSeparator>
764+
</li>,
765+
);
766+
}
758767
}
759768

760769
const cli = MatrixClientPeg.safeGet();
761770
let lastInSection = true;
762771
if (nextEventWithTile) {
763772
const nextEv = nextEventWithTile;
764-
const willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEv.getDate() || new Date());
773+
const willWantSeparator = this.wantsSeparator(mxEv, nextEv);
765774
lastInSection =
766-
willWantDateSeparator ||
775+
willWantSeparator === SeparatorKind.Date ||
767776
mxEv.getSender() !== nextEv.getSender() ||
768777
getEventDisplayInfo(cli, nextEv, this.showHiddenEvents).isInfoMessage ||
769778
!shouldFormContinuation(mxEv, nextEv, cli, this.showHiddenEvents, this.context.timelineRenderingType);
770779
}
771780

772781
// is this a continuation of the previous message?
773782
const continuation =
774-
!wantsDateSeparator &&
783+
wantsSeparator === SeparatorKind.None &&
775784
shouldFormContinuation(prevEvent, mxEv, cli, this.showHiddenEvents, this.context.timelineRenderingType);
776785

777786
const eventId = mxEv.getId()!;
@@ -816,16 +825,31 @@ export default class MessagePanel extends React.Component<IProps, IState> {
816825
return ret;
817826
}
818827

819-
public wantsDateSeparator(prevEvent: MatrixEvent | null, nextEventDate: Optional<Date>): boolean {
828+
public wantsSeparator(prevEvent: MatrixEvent | null, mxEvent: MatrixEvent): SeparatorKind {
820829
if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) {
821-
return false;
830+
return SeparatorKind.None;
822831
}
823-
if (prevEvent == null) {
824-
// first event in the panel: depends if we could back-paginate from
825-
// here.
826-
return !this.props.canBackPaginate;
832+
833+
if (prevEvent !== null) {
834+
// If the previous event was late but current is not then show a date separator for orientation
835+
// Otherwise if the current event is of a different late group than the previous show a late event separator
836+
const lateEventInfo = getLateEventInfo(mxEvent);
837+
if (lateEventInfo?.group_id !== getLateEventInfo(prevEvent)?.group_id) {
838+
return lateEventInfo !== undefined ? SeparatorKind.LateEvent : SeparatorKind.Date;
839+
}
840+
}
841+
842+
// first event in the panel: depends on if we could back-paginate from here.
843+
if (prevEvent === null && !this.props.canBackPaginate) {
844+
return SeparatorKind.Date;
827845
}
828-
return wantsDateSeparator(prevEvent.getDate() || undefined, nextEventDate);
846+
847+
const nextEventDate = mxEvent.getDate() ?? new Date();
848+
if (prevEvent !== null && wantsDateSeparator(prevEvent.getDate() || undefined, nextEventDate)) {
849+
return SeparatorKind.Date;
850+
}
851+
852+
return SeparatorKind.None;
829853
}
830854

831855
// Get a list of read receipts that should be shown next to this event

src/components/structures/grouper/CreationGrouper.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { _t } from "../../../languageHandler";
2525
import DateSeparator from "../../views/messages/DateSeparator";
2626
import NewRoomIntro from "../../views/rooms/NewRoomIntro";
2727
import GenericEventListSummary from "../../views/elements/GenericEventListSummary";
28+
import { SeparatorKind } from "../../views/messages/TimelineSeparator";
2829

2930
// Wrap initial room creation events into a GenericEventListSummary
3031
// Grouping only events sent by the same user that sent the `m.room.create` and only until
@@ -41,7 +42,7 @@ export class CreationGrouper extends BaseGrouper {
4142
if (!shouldShow) {
4243
return true;
4344
}
44-
if (panel.wantsDateSeparator(this.firstEventAndShouldShow.event, event.getDate())) {
45+
if (panel.wantsSeparator(this.firstEventAndShouldShow.event, event) === SeparatorKind.Date) {
4546
return false;
4647
}
4748
const eventType = event.getType();
@@ -96,7 +97,7 @@ export class CreationGrouper extends BaseGrouper {
9697
const createEvent = this.firstEventAndShouldShow;
9798
const lastShownEvent = this.lastShownEvent;
9899

99-
if (panel.wantsDateSeparator(this.prevEvent, createEvent.event.getDate())) {
100+
if (panel.wantsSeparator(this.prevEvent, createEvent.event) === SeparatorKind.Date) {
100101
const ts = createEvent.event.getTs();
101102
ret.push(
102103
<li key={ts + "~"}>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
18+
19+
const UNSIGNED_KEY = "io.element.late_event";
20+
21+
/**
22+
* This metadata describes when events arrive late after a net-split to offer improved UX.
23+
*/
24+
interface UnsignedLateEventInfo {
25+
/**
26+
* Milliseconds since epoch representing the time the event was received by the server
27+
*/
28+
received_at: number;
29+
/**
30+
* An opaque identifier representing the group the server has put the late arriving event into
31+
*/
32+
group_id: string;
33+
}
34+
35+
/**
36+
* Get io.element.late_event metadata from unsigned as sent by the server.
37+
*
38+
* @experimental this is not in the Matrix spec and needs special server support
39+
* @param mxEvent the Matrix Event to get UnsignedLateEventInfo on
40+
*/
41+
export function getLateEventInfo(mxEvent: MatrixEvent): UnsignedLateEventInfo | undefined {
42+
return mxEvent.getUnsigned()[UNSIGNED_KEY];
43+
}

src/components/structures/grouper/MainGrouper.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
2525
import DateSeparator from "../../views/messages/DateSeparator";
2626
import HistoryTile from "../../views/rooms/HistoryTile";
2727
import EventListSummary from "../../views/elements/EventListSummary";
28+
import { SeparatorKind } from "../../views/messages/TimelineSeparator";
2829

2930
const groupedStateEvents = [
3031
EventType.RoomMember,
@@ -70,7 +71,7 @@ export class MainGrouper extends BaseGrouper {
7071
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
7172
return true;
7273
}
73-
if (this.panel.wantsDateSeparator(this.events[0].event, ev.getDate())) {
74+
if (this.panel.wantsSeparator(this.events[0].event, ev) === SeparatorKind.Date) {
7475
return false;
7576
}
7677
if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) {
@@ -114,7 +115,7 @@ export class MainGrouper extends BaseGrouper {
114115
const lastShownEvent = this.lastShownEvent;
115116
const ret: ReactNode[] = [];
116117

117-
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].event.getDate())) {
118+
if (panel.wantsSeparator(this.prevEvent, this.events[0].event) === SeparatorKind.Date) {
118119
const ts = this.events[0].event.getTs();
119120
ret.push(
120121
<li key={ts + "~"}>

src/components/views/messages/DateSeparator.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import IconizedContextMenu, {
4040
import JumpToDatePicker from "./JumpToDatePicker";
4141
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
4242
import { SdkContextClass } from "../../../contexts/SDKContext";
43+
import TimelineSeparator from "./TimelineSeparator";
4344

4445
interface IProps {
4546
roomId: string;
@@ -52,6 +53,11 @@ interface IState {
5253
jumpToDateEnabled: boolean;
5354
}
5455

56+
/**
57+
* Timeline separator component to render within a MessagePanel bearing the date of the ts given
58+
*
59+
* Has additional jump to date functionality when labs flag is enabled
60+
*/
5561
export default class DateSeparator extends React.Component<IProps, IState> {
5662
private settingWatcherRef?: string;
5763

@@ -328,13 +334,6 @@ export default class DateSeparator extends React.Component<IProps, IState> {
328334
);
329335
}
330336

331-
// ARIA treats <hr/>s as separators, here we abuse them slightly so manually treat this entire thing as one
332-
return (
333-
<div className="mx_DateSeparator" role="separator" aria-label={label}>
334-
<hr role="none" />
335-
{dateHeaderContent}
336-
<hr role="none" />
337-
</div>
338-
);
337+
return <TimelineSeparator label={label}>{dateHeaderContent}</TimelineSeparator>;
339338
}
340339
}

0 commit comments

Comments
 (0)