diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index a54ceae49e9..8397353eb73 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -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 { diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index d248c6556fd..98f37c9e068 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -104,7 +104,9 @@ class FilePanel extends React.Component { } if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) { - this.state.timelineSet.addEventToTimeline(ev, timeline, false); + this.state.timelineSet.addEventToTimeline(ev, timeline, { + toStartOfTimeline: false, + }); } } diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 94b9905becc..02c4239e8a8 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -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'; @@ -79,6 +79,7 @@ interface IState { syncStateData: ISyncStateData; unsentMessages: MatrixEvent[]; isResending: boolean; + timelineNeedsRefresh: boolean; } export default class RoomStatusBar extends React.PureComponent { @@ -93,6 +94,7 @@ export default class RoomStatusBar extends React.PureComponent { syncStateData: this.context.getSyncStateData(), unsentMessages: getUnsentMessages(this.props.room), isResending: false, + timelineNeedsRefresh: this.props.room.getTimelineNeedsRefresh(), }; } @@ -100,12 +102,19 @@ export default class RoomStatusBar extends React.PureComponent { 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 { @@ -116,6 +125,8 @@ export default class RoomStatusBar extends React.PureComponent { 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 => { @@ -142,6 +153,15 @@ export default class RoomStatusBar extends React.PureComponent { dis.fire(Action.FocusSendMessageComposer); }; + private onRefreshTimelineClick = (): void => { + // Empty out the current timeline and re-request it + this.props.room.refreshLiveTimeline(); + + this.setState({ + timelineNeedsRefresh: false, + }); + }; + private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room) => { if (room.roomId !== this.props.room.roomId) return; const messages = getUnsentMessages(this.props.room); @@ -151,6 +171,14 @@ export default class RoomStatusBar extends React.PureComponent { }); }; + 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()) { @@ -166,7 +194,11 @@ export default class RoomStatusBar extends React.PureComponent { 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; @@ -286,8 +318,7 @@ export default class RoomStatusBar extends React.PureComponent { src={require("../../../res/img/feather-customised/warning-triangle.svg").default} width="24" height="24" - title="/!\ " - alt="/!\ " /> + alt="" />
{ _t('Connectivity to the server has been lost.') } @@ -306,6 +337,36 @@ export default class RoomStatusBar extends React.PureComponent { return this.getUnsentMessageContent(); } + if (this.state.timelineNeedsRefresh) { + return ( +
+
+
+ +
+
+
+ { _t("History import detected.") } +
+
+ { _t("History was just imported somewhere in the room. " + + "In order to see the historical messages, refresh your timeline.") } +
+
+
+ + { _t("Refresh timeline") } + +
+
+
+ ); + } + return null; } } diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index bc4ea5bdcbe..f37d4ebd36a 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -280,6 +280,7 @@ class TimelinePanel extends React.Component { 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. @@ -338,6 +339,14 @@ class TimelinePanel extends React.Component { } } + 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. @@ -370,6 +379,8 @@ class TimelinePanel extends React.Component { 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 => { @@ -627,10 +638,18 @@ class TimelinePanel extends React.Component { }); }; + 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()) { this.loadTimeline(); } }; @@ -1319,6 +1338,7 @@ class TimelinePanel extends React.Component { // get the list of events from the timeline window and the pending event list private getEvents(): Pick { 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 diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c5ecce8e81a..b5608aaa924 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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", diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index 85ff7038de0..3a958f2af6b 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -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, + }); } });