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

Ask to refresh timeline when historical messages are imported (MSC2716) #8303

6 changes: 6 additions & 0 deletions res/css/structures/_RoomStatusBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/retry.svg');
}
}

&.mx_RoomStatusBar_refreshTimelineBtn {
&::before {
mask-image: url('$(res)/img/element-icons/retry.svg');
}
}
}

.mx_InlineSpinner {
Expand Down
4 changes: 3 additions & 1 deletion src/components/structures/FilePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ class FilePanel extends React.Component<IProps, IState> {
}

if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) {
this.state.timelineSet.addEventToTimeline(ev, timeline, false);
this.state.timelineSet.addEventToTimeline(ev, timeline, {
toStartOfTimeline: false,
});
}
}

Expand Down
71 changes: 66 additions & 5 deletions src/components/structures/RoomStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SyncState, ISyncStateData } from "matrix-js-sdk/src/sync";
import { Room } from "matrix-js-sdk/src/models/room";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";

import { _t, _td } from '../../languageHandler';
import Resend from '../../Resend';
Expand Down Expand Up @@ -79,6 +79,7 @@ interface IState {
syncStateData: ISyncStateData;
unsentMessages: MatrixEvent[];
isResending: boolean;
timelineNeedsRefresh: boolean;
}

export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
Expand All @@ -93,19 +94,27 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
syncStateData: this.context.getSyncStateData(),
unsentMessages: getUnsentMessages(this.props.room),
isResending: false,
timelineNeedsRefresh: this.props.room.getTimelineNeedsRefresh(),
};
}

public componentDidMount(): void {
const client = this.context;
client.on("sync", this.onSyncStateChange);
client.on("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
this.props.room.on(RoomEvent.historyImportedWithinTimeline, this.onRoomHistoryImportedWithinTimeline);

this.checkSize();
}

public componentDidUpdate(): void {
public componentDidUpdate(prevProps): void {
this.checkSize();

// When the room changes, setup the new listener
if(prevProps.room !== this.props.room) {
prevProps.room.removeListener("Room.historyImportedWithinTimeline", this.onRoomHistoryImportedWithinTimeline);
this.props.room.on(RoomEvent.historyImportedWithinTimeline, this.onRoomHistoryImportedWithinTimeline);
}
}

public componentWillUnmount(): void {
Expand All @@ -116,6 +125,8 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
client.removeListener("sync", this.onSyncStateChange);
client.removeListener("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
}

this.props.room.removeListener(RoomEvent.historyImportedWithinTimeline, this.onRoomHistoryImportedWithinTimeline);
}

private onSyncStateChange = (state: SyncState, prevState: SyncState, data: ISyncStateData): void => {
Expand All @@ -142,6 +153,15 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
dis.fire(Action.FocusSendMessageComposer);
};

private onRefreshTimelineClick = (): void => {
// Empty out the current timeline and re-request it
this.props.room.refreshLiveTimeline();

this.setState({
timelineNeedsRefresh: false,
});
};
Copy link
Contributor Author

@MadLittleMods MadLittleMods Apr 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the behavior of all of this would be best served by an end-to-end test. It looks like we just added Cypress recently but it's missing all of the utility necessary to complete something like this.

  • Needs a command to create a user behind the scenes and visit Element as the user already signed in
  • Needs commands to setup rooms and send events
  • Needs a way to enable experimental_features in the homeserver.yaml template
  • Needs a way to add an application service registration in the Synapse instance. The MSC2716 /batch_send endpoint is only accessible from a AS token. No extra AS server needed, just the AS token configured.
  • Needs a way to interact with the homserver directly from the application service token to call /batch_send (probably via matrix-js-sdk)

I can't find an overall issue describing the need/want to use Cypress so it's unclear how much we want to move forward with it. I've had many troubles using Cypress with Gitter.

It seems like we have other e2e tests using Puppeteer but I'm guessing we want to move all of these to Cypress. Better place I should be adding some e2e tests or approaching this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conversation continued at #8354 (comment)


private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
if (room.roomId !== this.props.room.roomId) return;
const messages = getUnsentMessages(this.props.room);
Expand All @@ -151,6 +171,14 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
});
};

private onRoomHistoryImportedWithinTimeline = (markerEv: MatrixEvent, room: Room) => {
if (room.roomId !== this.props.room.roomId) return;

this.setState({
timelineNeedsRefresh: room.getTimelineNeedsRefresh(),
});
};

// Check whether current size is greater than 0, if yes call props.onVisible
private checkSize(): void {
if (this.getSize()) {
Expand All @@ -166,7 +194,11 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
private getSize(): number {
if (this.shouldShowConnectionError()) {
return STATUS_BAR_EXPANDED;
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
} else if (
this.state.unsentMessages.length > 0 ||
this.state.isResending ||
this.state.timelineNeedsRefresh
) {
return STATUS_BAR_EXPANDED_LARGE;
}
return STATUS_BAR_HIDDEN;
Expand Down Expand Up @@ -286,8 +318,7 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
src={require("../../../res/img/feather-customised/warning-triangle.svg").default}
width="24"
height="24"
title="/!\ "
alt="/!\ " />
alt="" />
<div>
<div className="mx_RoomStatusBar_connectionLostBar_title">
{ _t('Connectivity to the server has been lost.') }
Expand All @@ -306,6 +337,36 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
return this.getUnsentMessageContent();
}

if (this.state.timelineNeedsRefresh) {
return (
<div className="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages">
<div role="alert">
<div className="mx_RoomStatusBar_unsentBadge">
<img
src={require("../../../res/img/feather-customised/warning-triangle.svg").default}
width="24"
height="24"
alt="" />
</div>
<div>
<div className="mx_RoomStatusBar_unsentTitle">
{ _t("History import detected.") }
</div>
<div className="mx_RoomStatusBar_unsentDescription">
{ _t("History was just imported somewhere in the room. " +
"In order to see the historical messages, refresh your timeline.") }
</div>
</div>
<div className="mx_RoomStatusBar_unsentButtonBar">
<AccessibleButton onClick={this.onRefreshTimelineClick} className="mx_RoomStatusBar_refreshTimelineBtn">
{ _t("Refresh timeline") }
</AccessibleButton>
</div>
</div>
</div>
);
}

return null;
}
}
22 changes: 21 additions & 1 deletion src/components/structures/TimelinePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
const cli = MatrixClientPeg.get();
cli.on(RoomEvent.Timeline, this.onRoomTimeline);
cli.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
this.props.timelineSet.room.on(RoomEvent.TimelineRefresh, this.onRoomTimelineRefresh);
cli.on(RoomEvent.Redaction, this.onRoomRedaction);
if (SettingsStore.getValue("feature_msc3531_hide_messages_pending_moderation")) {
// Make sure that events are re-rendered when their visibility-pending-moderation changes.
Expand Down Expand Up @@ -338,6 +339,14 @@ class TimelinePanel extends React.Component<IProps, IState> {
}
}

public componentDidUpdate(prevProps): void {
// When the room changes, setup the new listener
if(prevProps.timelineSet.room !== this.props.timelineSet.room) {
prevProps.timelineSet.room.removeListener(RoomEvent.TimelineRefresh, this.onRoomTimelineRefresh);
this.props.timelineSet.room.on(RoomEvent.TimelineRefresh, this.onRoomTimelineRefresh);
}
}

componentWillUnmount() {
// set a boolean to say we've been unmounted, which any pending
// promises can use to throw away their results.
Expand Down Expand Up @@ -370,6 +379,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
client.removeListener(MatrixEventEvent.VisibilityChange, this.onEventVisibilityChange);
client.removeListener(ClientEvent.Sync, this.onSync);
}

this.props.timelineSet.room.removeListener(RoomEvent.TimelineRefresh, this.onRoomTimelineRefresh);
}

private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => {
Expand Down Expand Up @@ -627,10 +638,18 @@ class TimelinePanel extends React.Component<IProps, IState> {
});
};

private onRoomTimelineRefresh = (room: Room, timelineSet: EventTimelineSet): void => {
debuglog(`onRoomTimelineRefresh skipping=${timelineSet !== this.props.timelineSet}`);
if (timelineSet !== this.props.timelineSet) return;

this.refreshTimeline();
};

private onRoomTimelineReset = (room: Room, timelineSet: EventTimelineSet): void => {
debuglog(`onRoomTimelineReset skipping=${timelineSet !== this.props.timelineSet} skippingBecauseAtBottom=${this.canResetTimeline()}`);
if (timelineSet !== this.props.timelineSet) return;

if (this.messagePanel.current && this.messagePanel.current.isAtBottom()) {
if (this.canResetTimeline()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a missed refactor. canResetTimeline by name seems aplicable here and the logic appears pretty equivalent.

public canResetTimeline = () => {
if (!this.messagePanel) {
return true;
}
return this.messagePanel.canResetTimeline();
};

public canResetTimeline = () => this.messagePanel?.current.isAtBottom();

this.loadTimeline();
}
};
Expand Down Expand Up @@ -1319,6 +1338,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// get the list of events from the timeline window and the pending event list
private getEvents(): Pick<IState, "events" | "liveEvents" | "firstVisibleEventIndex"> {
const events: MatrixEvent[] = this.timelineWindow.getEvents();
console.log('TimelinePanel: getEvents', events.length);

// `arrayFastClone` performs a shallow copy of the array
// we want the last event to be decrypted first but displayed last
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -3058,6 +3058,9 @@
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"History import detected.": "History import detected.",
"History was just imported somewhere in the room. In order to see the historical messages, refresh your timeline.": "History was just imported somewhere in the room. In order to see the historical messages, refresh your timeline.",
"Refresh timeline": "Refresh timeline",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
"Search failed": "Search failed",
Expand Down
4 changes: 3 additions & 1 deletion src/indexing/EventIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,9 @@ export default class EventIndex extends EventEmitter {
// Add the events to the timeline of the file panel.
matrixEvents.forEach(e => {
if (!timelineSet.eventIdToTimeline(e.getId())) {
timelineSet.addEventToTimeline(e, timeline, direction == EventTimeline.BACKWARDS);
timelineSet.addEventToTimeline(e, timeline, {
toStartOfTimeline: direction == EventTimeline.BACKWARDS,
});
}
});

Expand Down