mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 19:56:47 +03:00
Fix: Remove jittery timeline scrolling after jumping to an event (#8263)
* Fix: Remove jittery timeline scrolling after jumping to an event * Fix: Remove onUserScroll handler and merge it with onScroll * Fix: Reset scrollIntoView state earlier Co-authored-by: Janne Mareike Koschinski <jannemk@element.io>
This commit is contained in:
parent
285dc25b3e
commit
579a166113
11 changed files with 118 additions and 87 deletions
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
|
import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
@ -170,9 +170,6 @@ interface IProps {
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll?(event: Event): void;
|
onScroll?(event: Event): void;
|
||||||
|
|
||||||
// callback which is called when the user interacts with the room timeline
|
|
||||||
onUserScroll(event: SyntheticEvent): void;
|
|
||||||
|
|
||||||
// callback which is called when more content is needed.
|
// callback which is called when more content is needed.
|
||||||
onFillRequest?(backwards: boolean): Promise<boolean>;
|
onFillRequest?(backwards: boolean): Promise<boolean>;
|
||||||
|
|
||||||
|
@ -1030,7 +1027,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
ref={this.scrollPanel}
|
ref={this.scrollPanel}
|
||||||
className={classes}
|
className={classes}
|
||||||
onScroll={this.props.onScroll}
|
onScroll={this.props.onScroll}
|
||||||
onUserScroll={this.props.onUserScroll}
|
|
||||||
onFillRequest={this.props.onFillRequest}
|
onFillRequest={this.props.onFillRequest}
|
||||||
onUnfillRequest={this.props.onUnfillRequest}
|
onUnfillRequest={this.props.onUnfillRequest}
|
||||||
style={style}
|
style={style}
|
||||||
|
|
|
@ -231,6 +231,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
mxEvent={cardState.threadHeadEvent}
|
mxEvent={cardState.threadHeadEvent}
|
||||||
initialEvent={cardState.initialEvent}
|
initialEvent={cardState.initialEvent}
|
||||||
isInitialEventHighlighted={cardState.isInitialEventHighlighted}
|
isInitialEventHighlighted={cardState.isInitialEventHighlighted}
|
||||||
|
initialEventScrollIntoView={cardState.initialEventScrollIntoView}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
e2eStatus={this.props.e2eStatus}
|
e2eStatus={this.props.e2eStatus}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -155,6 +155,8 @@ export interface IRoomState {
|
||||||
initialEventPixelOffset?: number;
|
initialEventPixelOffset?: number;
|
||||||
// Whether to highlight the event scrolled to
|
// Whether to highlight the event scrolled to
|
||||||
isInitialEventHighlighted?: boolean;
|
isInitialEventHighlighted?: boolean;
|
||||||
|
// Whether to scroll the event into view
|
||||||
|
initialEventScrollIntoView?: boolean;
|
||||||
replyToEvent?: MatrixEvent;
|
replyToEvent?: MatrixEvent;
|
||||||
numUnreadMessages: number;
|
numUnreadMessages: number;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
|
@ -404,7 +406,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
|
|
||||||
const roomId = RoomViewStore.instance.getRoomId();
|
const roomId = RoomViewStore.instance.getRoomId();
|
||||||
|
|
||||||
const newState: Pick<IRoomState, any> = {
|
// This convoluted type signature ensures we get IntelliSense *and* correct typing
|
||||||
|
const newState: Partial<IRoomState> & Pick<IRoomState, any> = {
|
||||||
roomId,
|
roomId,
|
||||||
roomAlias: RoomViewStore.instance.getRoomAlias(),
|
roomAlias: RoomViewStore.instance.getRoomAlias(),
|
||||||
roomLoading: RoomViewStore.instance.isRoomLoading(),
|
roomLoading: RoomViewStore.instance.isRoomLoading(),
|
||||||
|
@ -443,22 +446,29 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have an initial event, we want to reset the event pixel offset to ensure it ends up
|
||||||
|
// visible
|
||||||
|
newState.initialEventPixelOffset = null;
|
||||||
|
|
||||||
const thread = initialEvent?.getThread();
|
const thread = initialEvent?.getThread();
|
||||||
if (thread && !initialEvent?.isThreadRoot) {
|
if (thread && !initialEvent?.isThreadRoot) {
|
||||||
showThread({
|
showThread({
|
||||||
rootEvent: thread.rootEvent,
|
rootEvent: thread.rootEvent,
|
||||||
initialEvent,
|
initialEvent,
|
||||||
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
||||||
|
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
newState.initialEventId = initialEventId;
|
newState.initialEventId = initialEventId;
|
||||||
newState.isInitialEventHighlighted = RoomViewStore.instance.isInitialEventHighlighted();
|
newState.isInitialEventHighlighted = RoomViewStore.instance.isInitialEventHighlighted();
|
||||||
|
newState.initialEventScrollIntoView = RoomViewStore.instance.initialEventScrollIntoView();
|
||||||
|
|
||||||
if (thread && initialEvent?.isThreadRoot) {
|
if (thread && initialEvent?.isThreadRoot) {
|
||||||
showThread({
|
showThread({
|
||||||
rootEvent: thread.rootEvent,
|
rootEvent: thread.rootEvent,
|
||||||
initialEvent,
|
initialEvent,
|
||||||
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
highlighted: RoomViewStore.instance.isInitialEventHighlighted(),
|
||||||
|
scroll_into_view: RoomViewStore.instance.initialEventScrollIntoView(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -758,19 +768,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onUserScroll = () => {
|
|
||||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
|
||||||
dis.dispatch<ViewRoomPayload>({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: this.state.room.roomId,
|
|
||||||
event_id: this.state.initialEventId,
|
|
||||||
highlighted: false,
|
|
||||||
replyingToEvent: this.state.replyToEvent,
|
|
||||||
metricsTrigger: undefined, // room doesn't change
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRightPanelStoreUpdate = () => {
|
private onRightPanelStoreUpdate = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.state.roomId),
|
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.state.roomId),
|
||||||
|
@ -1301,6 +1298,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
this.updateTopUnreadMessagesBar();
|
this.updateTopUnreadMessagesBar();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private resetJumpToEvent = (eventId?: string) => {
|
||||||
|
if (this.state.initialEventId && this.state.initialEventScrollIntoView &&
|
||||||
|
this.state.initialEventId === eventId) {
|
||||||
|
debuglog("Removing scroll_into_view flag from initial event");
|
||||||
|
dis.dispatch<ViewRoomPayload>({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: this.state.room.roomId,
|
||||||
|
event_id: this.state.initialEventId,
|
||||||
|
highlighted: this.state.isInitialEventHighlighted,
|
||||||
|
scroll_into_view: false,
|
||||||
|
replyingToEvent: this.state.replyToEvent,
|
||||||
|
metricsTrigger: undefined, // room doesn't change
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private injectSticker(url: string, info: object, text: string, threadId: string | null) {
|
private injectSticker(url: string, info: object, text: string, threadId: string | null) {
|
||||||
if (this.context.isGuest()) {
|
if (this.context.isGuest()) {
|
||||||
dis.dispatch({ action: 'require_registration' });
|
dis.dispatch({ action: 'require_registration' });
|
||||||
|
@ -2051,9 +2064,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
hidden={hideMessagePanel}
|
hidden={hideMessagePanel}
|
||||||
highlightedEventId={highlightedEventId}
|
highlightedEventId={highlightedEventId}
|
||||||
eventId={this.state.initialEventId}
|
eventId={this.state.initialEventId}
|
||||||
|
eventScrollIntoView={this.state.initialEventScrollIntoView}
|
||||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||||
onScroll={this.onMessageListScroll}
|
onScroll={this.onMessageListScroll}
|
||||||
onUserScroll={this.onUserScroll}
|
onEventScrolledIntoView={this.resetJumpToEvent}
|
||||||
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
|
||||||
showUrlPreview={this.state.showUrlPreview}
|
showUrlPreview={this.state.showUrlPreview}
|
||||||
className={this.messagePanelClassNames}
|
className={this.messagePanelClassNames}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
|
import React, { createRef, CSSProperties, ReactNode, KeyboardEvent } from "react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import Timer from '../../utils/Timer';
|
import Timer from '../../utils/Timer';
|
||||||
|
@ -109,10 +109,6 @@ interface IProps {
|
||||||
/* onScroll: a callback which is called whenever any scroll happens.
|
/* onScroll: a callback which is called whenever any scroll happens.
|
||||||
*/
|
*/
|
||||||
onScroll?(event: Event): void;
|
onScroll?(event: Event): void;
|
||||||
|
|
||||||
/* onUserScroll: callback which is called when the user interacts with the room timeline
|
|
||||||
*/
|
|
||||||
onUserScroll?(event: SyntheticEvent): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This component implements an intelligent scrolling list.
|
/* This component implements an intelligent scrolling list.
|
||||||
|
@ -593,29 +589,21 @@ export default class ScrollPanel extends React.Component<IProps> {
|
||||||
* @param {object} ev the keyboard event
|
* @param {object} ev the keyboard event
|
||||||
*/
|
*/
|
||||||
public handleScrollKey = (ev: KeyboardEvent) => {
|
public handleScrollKey = (ev: KeyboardEvent) => {
|
||||||
let isScrolling = false;
|
|
||||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||||
switch (roomAction) {
|
switch (roomAction) {
|
||||||
case KeyBindingAction.ScrollUp:
|
case KeyBindingAction.ScrollUp:
|
||||||
this.scrollRelative(-1);
|
this.scrollRelative(-1);
|
||||||
isScrolling = true;
|
|
||||||
break;
|
break;
|
||||||
case KeyBindingAction.ScrollDown:
|
case KeyBindingAction.ScrollDown:
|
||||||
this.scrollRelative(1);
|
this.scrollRelative(1);
|
||||||
isScrolling = true;
|
|
||||||
break;
|
break;
|
||||||
case KeyBindingAction.JumpToFirstMessage:
|
case KeyBindingAction.JumpToFirstMessage:
|
||||||
this.scrollToTop();
|
this.scrollToTop();
|
||||||
isScrolling = true;
|
|
||||||
break;
|
break;
|
||||||
case KeyBindingAction.JumpToLatestMessage:
|
case KeyBindingAction.JumpToLatestMessage:
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
isScrolling = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (isScrolling && this.props.onUserScroll) {
|
|
||||||
this.props.onUserScroll(ev);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Scroll the panel to bring the DOM node with the scroll token
|
/* Scroll the panel to bring the DOM node with the scroll token
|
||||||
|
@ -965,7 +953,6 @@ export default class ScrollPanel extends React.Component<IProps> {
|
||||||
<AutoHideScrollbar
|
<AutoHideScrollbar
|
||||||
wrappedRef={this.collectScroll}
|
wrappedRef={this.collectScroll}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
onWheel={this.props.onUserScroll}
|
|
||||||
className={`mx_ScrollPanel ${this.props.className}`}
|
className={`mx_ScrollPanel ${this.props.className}`}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
>
|
>
|
||||||
|
|
|
@ -61,6 +61,7 @@ interface IProps {
|
||||||
e2eStatus?: E2EStatus;
|
e2eStatus?: E2EStatus;
|
||||||
initialEvent?: MatrixEvent;
|
initialEvent?: MatrixEvent;
|
||||||
isInitialEventHighlighted?: boolean;
|
isInitialEventHighlighted?: boolean;
|
||||||
|
initialEventScrollIntoView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -215,13 +216,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private resetHighlightedEvent = (): void => {
|
private resetJumpToEvent = (event?: string): void => {
|
||||||
if (this.props.initialEvent && this.props.isInitialEventHighlighted) {
|
if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
|
||||||
|
event === this.props.initialEvent?.getId()) {
|
||||||
dis.dispatch<ViewRoomPayload>({
|
dis.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
event_id: this.props.initialEvent?.getId(),
|
event_id: this.props.initialEvent?.getId(),
|
||||||
highlighted: false,
|
highlighted: this.props.isInitialEventHighlighted,
|
||||||
|
scroll_into_view: false,
|
||||||
replyingToEvent: this.state.replyToEvent,
|
replyingToEvent: this.state.replyToEvent,
|
||||||
metricsTrigger: undefined, // room doesn't change
|
metricsTrigger: undefined, // room doesn't change
|
||||||
});
|
});
|
||||||
|
@ -372,7 +375,8 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
editState={this.state.editState}
|
editState={this.state.editState}
|
||||||
eventId={this.props.initialEvent?.getId()}
|
eventId={this.props.initialEvent?.getId()}
|
||||||
highlightedEventId={highlightedEventId}
|
highlightedEventId={highlightedEventId}
|
||||||
onUserScroll={this.resetHighlightedEvent}
|
eventScrollIntoView={this.props.initialEventScrollIntoView}
|
||||||
|
onEventScrolledIntoView={this.resetJumpToEvent}
|
||||||
onPaginationRequest={this.onPaginationRequest}
|
onPaginationRequest={this.onPaginationRequest}
|
||||||
/>
|
/>
|
||||||
</div> }
|
</div> }
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, ReactNode, SyntheticEvent } from 'react';
|
import React, { createRef, ReactNode } from 'react';
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
@ -91,6 +91,9 @@ interface IProps {
|
||||||
// id of an event to jump to. If not given, will go to the end of the live timeline.
|
// id of an event to jump to. If not given, will go to the end of the live timeline.
|
||||||
eventId?: string;
|
eventId?: string;
|
||||||
|
|
||||||
|
// whether we should scroll the event into view
|
||||||
|
eventScrollIntoView?: boolean;
|
||||||
|
|
||||||
// where to position the event given by eventId, in pixels from the bottom of the viewport.
|
// where to position the event given by eventId, in pixels from the bottom of the viewport.
|
||||||
// If not given, will try to put the event half way down the viewport.
|
// If not given, will try to put the event half way down the viewport.
|
||||||
eventPixelOffset?: number;
|
eventPixelOffset?: number;
|
||||||
|
@ -124,8 +127,7 @@ interface IProps {
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll?(event: Event): void;
|
onScroll?(event: Event): void;
|
||||||
|
|
||||||
// callback which is called when the user interacts with the room timeline
|
onEventScrolledIntoView?(eventId?: string): void;
|
||||||
onUserScroll?(event: SyntheticEvent): void;
|
|
||||||
|
|
||||||
// callback which is called when the read-up-to mark is updated.
|
// callback which is called when the read-up-to mark is updated.
|
||||||
onReadMarkerUpdated?(): void;
|
onReadMarkerUpdated?(): void;
|
||||||
|
@ -327,9 +329,11 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const differentEventId = newProps.eventId != this.props.eventId;
|
const differentEventId = newProps.eventId != this.props.eventId;
|
||||||
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
|
const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
|
||||||
if (differentEventId || differentHighlightedEventId) {
|
const differentAvoidJump = newProps.eventScrollIntoView && !this.props.eventScrollIntoView;
|
||||||
logger.log("TimelinePanel switching to eventId " + newProps.eventId +
|
if (differentEventId || differentHighlightedEventId || differentAvoidJump) {
|
||||||
" (was " + this.props.eventId + ")");
|
logger.log("TimelinePanel switching to " +
|
||||||
|
"eventId " + newProps.eventId + " (was " + this.props.eventId + "), " +
|
||||||
|
"scrollIntoView: " + newProps.eventScrollIntoView + " (was " + this.props.eventScrollIntoView + ")");
|
||||||
return this.initTimeline(newProps);
|
return this.initTimeline(newProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1123,7 +1127,41 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
offsetBase = 0.5;
|
offsetBase = 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.loadTimeline(initialEvent, pixelOffset, offsetBase);
|
return this.loadTimeline(initialEvent, pixelOffset, offsetBase, props.eventScrollIntoView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollIntoView(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
|
||||||
|
const doScroll = () => {
|
||||||
|
if (eventId) {
|
||||||
|
debuglog("TimelinePanel scrolling to eventId " + eventId +
|
||||||
|
" at position " + (offsetBase * 100) + "% + " + pixelOffset);
|
||||||
|
this.messagePanel.current.scrollToEvent(
|
||||||
|
eventId,
|
||||||
|
pixelOffset,
|
||||||
|
offsetBase,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debuglog("TimelinePanel scrolling to bottom");
|
||||||
|
this.messagePanel.current.scrollToBottom();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
debuglog("TimelinePanel scheduling scroll to event");
|
||||||
|
this.props.onEventScrolledIntoView?.(eventId);
|
||||||
|
// Ensure the correct scroll position pre render, if the messages have already been loaded to DOM,
|
||||||
|
// to avoid it jumping around
|
||||||
|
doScroll();
|
||||||
|
|
||||||
|
// Ensure the correct scroll position post render for correct behaviour.
|
||||||
|
//
|
||||||
|
// requestAnimationFrame runs our code immediately after the DOM update but before the next repaint.
|
||||||
|
//
|
||||||
|
// If the messages have just been loaded for the first time, this ensures we'll repeat setting the
|
||||||
|
// correct scroll position after React has re-rendered the TimelinePanel and MessagePanel and
|
||||||
|
// updated the DOM.
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
doScroll();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1139,8 +1177,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
* @param {number?} offsetBase the reference point for the pixelOffset. 0
|
* @param {number?} offsetBase the reference point for the pixelOffset. 0
|
||||||
* means the top of the container, 1 means the bottom, and fractional
|
* means the top of the container, 1 means the bottom, and fractional
|
||||||
* values mean somewhere in the middle. If omitted, it defaults to 0.
|
* values mean somewhere in the middle. If omitted, it defaults to 0.
|
||||||
|
*
|
||||||
|
* @param {boolean?} scrollIntoView whether to scroll the event into view.
|
||||||
*/
|
*/
|
||||||
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
|
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void {
|
||||||
this.timelineWindow = new TimelineWindow(
|
this.timelineWindow = new TimelineWindow(
|
||||||
MatrixClientPeg.get(), this.props.timelineSet,
|
MatrixClientPeg.get(), this.props.timelineSet,
|
||||||
{ windowLimit: this.props.timelineCap });
|
{ windowLimit: this.props.timelineCap });
|
||||||
|
@ -1176,32 +1216,9 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doScroll = () => {
|
if (scrollIntoView) {
|
||||||
if (eventId) {
|
this.scrollIntoView(eventId, pixelOffset, offsetBase);
|
||||||
debuglog("TimelinePanel scrolling to eventId " + eventId);
|
}
|
||||||
this.messagePanel.current.scrollToEvent(
|
|
||||||
eventId,
|
|
||||||
pixelOffset,
|
|
||||||
offsetBase,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
debuglog("TimelinePanel scrolling to bottom");
|
|
||||||
this.messagePanel.current.scrollToBottom();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure the correct scroll position pre render, if the messages have already been loaded to DOM, to
|
|
||||||
// avoid it jumping around
|
|
||||||
doScroll();
|
|
||||||
|
|
||||||
// Ensure the correct scroll position post render for correct behaviour.
|
|
||||||
//
|
|
||||||
// requestAnimationFrame runs our code immediately after the DOM update but before the next repaint.
|
|
||||||
//
|
|
||||||
// If the messages have just been loaded for the first time, this ensures we'll repeat setting the
|
|
||||||
// correct scroll position after React has re-rendered the TimelinePanel and MessagePanel and updated
|
|
||||||
// the DOM.
|
|
||||||
window.requestAnimationFrame(doScroll);
|
|
||||||
|
|
||||||
if (this.props.sendReadReceiptOnLoad) {
|
if (this.props.sendReadReceiptOnLoad) {
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
|
@ -1651,7 +1668,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||||
stickyBottom={stickyBottom}
|
stickyBottom={stickyBottom}
|
||||||
onScroll={this.onMessageListScroll}
|
onScroll={this.onMessageListScroll}
|
||||||
onUserScroll={this.props.onUserScroll}
|
|
||||||
onFillRequest={this.onMessageListFillRequest}
|
onFillRequest={this.onMessageListFillRequest}
|
||||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||||
isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour}
|
isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour}
|
||||||
|
|
|
@ -146,19 +146,6 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onUserScroll = (): void => {
|
|
||||||
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
|
||||||
dis.dispatch<ViewRoomPayload>({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: this.props.room.roomId,
|
|
||||||
event_id: this.state.initialEventId,
|
|
||||||
highlighted: false,
|
|
||||||
replyingToEvent: this.state.replyToEvent,
|
|
||||||
metricsTrigger: undefined, // room doesn't change
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onScroll = (): void => {
|
private onScroll = (): void => {
|
||||||
const timelinePanel = this.timelinePanel.current;
|
const timelinePanel = this.timelinePanel.current;
|
||||||
if (!timelinePanel) return;
|
if (!timelinePanel) return;
|
||||||
|
@ -171,6 +158,17 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||||
atEndOfLiveTimeline: false,
|
atEndOfLiveTimeline: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
|
||||||
|
dis.dispatch<ViewRoomPayload>({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: this.props.room.roomId,
|
||||||
|
event_id: this.state.initialEventId,
|
||||||
|
highlighted: false,
|
||||||
|
replyingToEvent: this.state.replyToEvent,
|
||||||
|
metricsTrigger: undefined, // room doesn't change
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMeasurement = (narrow: boolean): void => {
|
private onMeasurement = (narrow: boolean): void => {
|
||||||
|
@ -263,7 +261,6 @@ export default class TimelineCard extends React.Component<IProps, IState> {
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
highlightedEventId={highlightedEventId}
|
highlightedEventId={highlightedEventId}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
onUserScroll={this.onUserScroll}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ export const showThread = (props: {
|
||||||
rootEvent: MatrixEvent;
|
rootEvent: MatrixEvent;
|
||||||
initialEvent?: MatrixEvent;
|
initialEvent?: MatrixEvent;
|
||||||
highlighted?: boolean;
|
highlighted?: boolean;
|
||||||
|
scroll_into_view?: boolean;
|
||||||
push?: boolean;
|
push?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const push = props.push ?? false;
|
const push = props.push ?? false;
|
||||||
|
@ -35,6 +36,7 @@ export const showThread = (props: {
|
||||||
threadHeadEvent: props.rootEvent,
|
threadHeadEvent: props.rootEvent,
|
||||||
initialEvent: props.initialEvent,
|
initialEvent: props.initialEvent,
|
||||||
isInitialEventHighlighted: props.highlighted,
|
isInitialEventHighlighted: props.highlighted,
|
||||||
|
initialEventScrollIntoView: props.scroll_into_view,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (push) {
|
if (push) {
|
||||||
|
|
|
@ -32,6 +32,7 @@ export interface ViewRoomPayload extends Pick<ActionPayload, "action"> {
|
||||||
|
|
||||||
event_id?: string; // the event to ensure is in view if any
|
event_id?: string; // the event to ensure is in view if any
|
||||||
highlighted?: boolean; // whether to highlight `event_id`
|
highlighted?: boolean; // whether to highlight `event_id`
|
||||||
|
scroll_into_view?: boolean; // whether to scroll `event_id` into view
|
||||||
should_peek?: boolean; // whether we should peek the room if we are not yet joined
|
should_peek?: boolean; // whether we should peek the room if we are not yet joined
|
||||||
joining?: boolean; // whether we have already sent a join request for this room
|
joining?: boolean; // whether we have already sent a join request for this room
|
||||||
via_servers?: string[]; // the list of servers to join via if no room_alias is provided
|
via_servers?: string[]; // the list of servers to join via if no room_alias is provided
|
||||||
|
|
|
@ -62,6 +62,8 @@ const INITIAL_STATE = {
|
||||||
initialEventPixelOffset: null,
|
initialEventPixelOffset: null,
|
||||||
// Whether to highlight the initial event
|
// Whether to highlight the initial event
|
||||||
isInitialEventHighlighted: false,
|
isInitialEventHighlighted: false,
|
||||||
|
// whether to scroll `event_id` into view
|
||||||
|
initialEventScrollIntoView: true,
|
||||||
|
|
||||||
// The room alias of the room (or null if not originally specified in view_room)
|
// The room alias of the room (or null if not originally specified in view_room)
|
||||||
roomAlias: null,
|
roomAlias: null,
|
||||||
|
@ -291,6 +293,7 @@ export class RoomViewStore extends Store<ActionPayload> {
|
||||||
roomAlias: payload.room_alias,
|
roomAlias: payload.room_alias,
|
||||||
initialEventId: payload.event_id,
|
initialEventId: payload.event_id,
|
||||||
isInitialEventHighlighted: payload.highlighted,
|
isInitialEventHighlighted: payload.highlighted,
|
||||||
|
initialEventScrollIntoView: payload.scroll_into_view ?? true,
|
||||||
roomLoading: false,
|
roomLoading: false,
|
||||||
roomLoadError: null,
|
roomLoadError: null,
|
||||||
// should peek by default
|
// should peek by default
|
||||||
|
@ -333,6 +336,7 @@ export class RoomViewStore extends Store<ActionPayload> {
|
||||||
initialEventId: null,
|
initialEventId: null,
|
||||||
initialEventPixelOffset: null,
|
initialEventPixelOffset: null,
|
||||||
isInitialEventHighlighted: null,
|
isInitialEventHighlighted: null,
|
||||||
|
initialEventScrollIntoView: true,
|
||||||
roomAlias: payload.room_alias,
|
roomAlias: payload.room_alias,
|
||||||
roomLoading: true,
|
roomLoading: true,
|
||||||
roomLoadError: null,
|
roomLoadError: null,
|
||||||
|
@ -475,6 +479,11 @@ export class RoomViewStore extends Store<ActionPayload> {
|
||||||
return this.state.isInitialEventHighlighted;
|
return this.state.isInitialEventHighlighted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether to avoid jumping to the initial event
|
||||||
|
public initialEventScrollIntoView() {
|
||||||
|
return this.state.initialEventScrollIntoView;
|
||||||
|
}
|
||||||
|
|
||||||
// The room alias of the room (or null if not originally specified in view_room)
|
// The room alias of the room (or null if not originally specified in view_room)
|
||||||
public getRoomAlias() {
|
public getRoomAlias() {
|
||||||
return this.state.roomAlias;
|
return this.state.roomAlias;
|
||||||
|
|
|
@ -34,6 +34,7 @@ export interface IRightPanelCardState {
|
||||||
threadHeadEvent?: MatrixEvent;
|
threadHeadEvent?: MatrixEvent;
|
||||||
initialEvent?: MatrixEvent;
|
initialEvent?: MatrixEvent;
|
||||||
isInitialEventHighlighted?: boolean;
|
isInitialEventHighlighted?: boolean;
|
||||||
|
initialEventScrollIntoView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRightPanelCardStateStored {
|
export interface IRightPanelCardStateStored {
|
||||||
|
@ -47,6 +48,7 @@ export interface IRightPanelCardStateStored {
|
||||||
threadHeadEventId?: string;
|
threadHeadEventId?: string;
|
||||||
initialEventId?: string;
|
initialEventId?: string;
|
||||||
isInitialEventHighlighted?: boolean;
|
isInitialEventHighlighted?: boolean;
|
||||||
|
initialEventScrollIntoView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRightPanelCard {
|
export interface IRightPanelCard {
|
||||||
|
@ -87,6 +89,7 @@ export function convertCardToStore(panelState: IRightPanelCard): IRightPanelCard
|
||||||
widgetId: state.widgetId,
|
widgetId: state.widgetId,
|
||||||
spaceId: state.spaceId,
|
spaceId: state.spaceId,
|
||||||
isInitialEventHighlighted: state.isInitialEventHighlighted,
|
isInitialEventHighlighted: state.isInitialEventHighlighted,
|
||||||
|
initialEventScrollIntoView: state.initialEventScrollIntoView,
|
||||||
threadHeadEventId: !!state?.threadHeadEvent?.getId() ?
|
threadHeadEventId: !!state?.threadHeadEvent?.getId() ?
|
||||||
panelState.state.threadHeadEvent.getId() : undefined,
|
panelState.state.threadHeadEvent.getId() : undefined,
|
||||||
memberInfoEventId: !!state?.memberInfoEvent?.getId() ?
|
memberInfoEventId: !!state?.memberInfoEvent?.getId() ?
|
||||||
|
@ -106,6 +109,7 @@ function convertStoreToCard(panelStateStore: IRightPanelCardStored, room: Room):
|
||||||
widgetId: stateStored.widgetId,
|
widgetId: stateStored.widgetId,
|
||||||
spaceId: stateStored.spaceId,
|
spaceId: stateStored.spaceId,
|
||||||
isInitialEventHighlighted: stateStored.isInitialEventHighlighted,
|
isInitialEventHighlighted: stateStored.isInitialEventHighlighted,
|
||||||
|
initialEventScrollIntoView: stateStored.initialEventScrollIntoView,
|
||||||
threadHeadEvent: !!stateStored?.threadHeadEventId ?
|
threadHeadEvent: !!stateStored?.threadHeadEventId ?
|
||||||
room.findEventById(stateStored.threadHeadEventId) : undefined,
|
room.findEventById(stateStored.threadHeadEventId) : undefined,
|
||||||
memberInfoEvent: !!stateStored?.memberInfoEventId ?
|
memberInfoEvent: !!stateStored?.memberInfoEventId ?
|
||||||
|
|
Loading…
Reference in a new issue