Fix timeline not scrolling to bottom reliably
This commit is contained in:
parent
feef67da5a
commit
4b6b9cce01
3 changed files with 59 additions and 18 deletions
|
@ -5471,6 +5471,21 @@ button.module-image__border-overlay:focus {
|
||||||
.module-timeline__messages {
|
.module-timeline__messages {
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
|
|
||||||
|
// This is a modified version of ["Pin Scrolling to Bottom"][0].
|
||||||
|
// [0]: https://css-tricks.com/books/greatest-css-tricks/pin-scrolling-to-bottom/
|
||||||
|
&--have-newest {
|
||||||
|
& > * {
|
||||||
|
overflow-anchor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
height: 1px;
|
||||||
|
display: block;
|
||||||
|
overflow-anchor: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactVirtualized__List {
|
.ReactVirtualized__List {
|
||||||
|
|
|
@ -92,7 +92,19 @@ type Trigger = {
|
||||||
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_METADATA_WIDTH = 20;
|
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10;
|
||||||
|
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
|
||||||
|
const GUESS_METADATA_WIDTH_OUTGOING_SIZE: Record<MessageStatusType, number> = {
|
||||||
|
delivered: 24,
|
||||||
|
error: 24,
|
||||||
|
paused: 18,
|
||||||
|
'partial-sent': 24,
|
||||||
|
read: 24,
|
||||||
|
sending: 18,
|
||||||
|
sent: 24,
|
||||||
|
viewed: 24,
|
||||||
|
};
|
||||||
|
|
||||||
const EXPIRATION_CHECK_MINIMUM = 2000;
|
const EXPIRATION_CHECK_MINIMUM = 2000;
|
||||||
const EXPIRED_DELAY = 600;
|
const EXPIRED_DELAY = 600;
|
||||||
const GROUP_AVATAR_SIZE = AvatarSize.TWENTY_EIGHT;
|
const GROUP_AVATAR_SIZE = AvatarSize.TWENTY_EIGHT;
|
||||||
|
@ -352,7 +364,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
metadataWidth: DEFAULT_METADATA_WIDTH,
|
metadataWidth: this.guessMetadataWidth(),
|
||||||
|
|
||||||
expiring: false,
|
expiring: false,
|
||||||
expired: false,
|
expired: false,
|
||||||
|
@ -552,6 +564,32 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
return MetadataPlacement.InlineWithText;
|
return MetadataPlacement.InlineWithText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lot of the time, we add an invisible inline spacer for messages. This spacer is the
|
||||||
|
* same size as the message metadata. Unfortunately, we don't know how wide it is until
|
||||||
|
* we render it.
|
||||||
|
*
|
||||||
|
* This will probably guess wrong, but it's valuable to get close to the real value
|
||||||
|
* because it can reduce layout jumpiness.
|
||||||
|
*/
|
||||||
|
private guessMetadataWidth(): number {
|
||||||
|
const { direction, expirationLength, expirationTimestamp, status } =
|
||||||
|
this.props;
|
||||||
|
|
||||||
|
let result = GUESS_METADATA_WIDTH_TIMESTAMP_SIZE;
|
||||||
|
|
||||||
|
const hasExpireTimer = Boolean(expirationLength && expirationTimestamp);
|
||||||
|
if (hasExpireTimer) {
|
||||||
|
result += GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction === 'outgoing' && status) {
|
||||||
|
result += GUESS_METADATA_WIDTH_OUTGOING_SIZE[status];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public startSelectedTimer(): void {
|
public startSelectedTimer(): void {
|
||||||
const { clearSelectedMessage, interactionMode } = this.props;
|
const { clearSelectedMessage, interactionMode } = this.props;
|
||||||
const { isSelected } = this.state;
|
const { isSelected } = this.state;
|
||||||
|
|
|
@ -263,7 +263,6 @@ export class Timeline extends React.Component<
|
||||||
private readonly messagesRef = React.createRef<HTMLDivElement>();
|
private readonly messagesRef = React.createRef<HTMLDivElement>();
|
||||||
private readonly lastSeenIndicatorRef = React.createRef<HTMLDivElement>();
|
private readonly lastSeenIndicatorRef = React.createRef<HTMLDivElement>();
|
||||||
private intersectionObserver?: IntersectionObserver;
|
private intersectionObserver?: IntersectionObserver;
|
||||||
private messagesResizeObserver?: ResizeObserver;
|
|
||||||
|
|
||||||
// This is a best guess. It will likely be overridden when the timeline is measured.
|
// This is a best guess. It will likely be overridden when the timeline is measured.
|
||||||
private maxVisibleRows = Math.ceil(window.innerHeight / MIN_ROW_HEIGHT);
|
private maxVisibleRows = Math.ceil(window.innerHeight / MIN_ROW_HEIGHT);
|
||||||
|
@ -490,19 +489,6 @@ export class Timeline extends React.Component<
|
||||||
'<Timeline> mounted without some refs'
|
'<Timeline> mounted without some refs'
|
||||||
);
|
);
|
||||||
|
|
||||||
// This observer is necessary to keep the scroll position locked to the bottom when
|
|
||||||
// messages change height without "telling" the timeline about it. This can happen
|
|
||||||
// if messages animate their height, if reactions are changed, etc.
|
|
||||||
//
|
|
||||||
// We do this synchronously (i.e., without react-measure) to avoid jitter.
|
|
||||||
this.messagesResizeObserver = new ResizeObserver(() => {
|
|
||||||
const { haveNewest } = this.props;
|
|
||||||
if (haveNewest && this.isAtBottom()) {
|
|
||||||
scrollToBottom(containerEl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.messagesResizeObserver.observe(messagesEl);
|
|
||||||
|
|
||||||
this.updateIntersectionObserver();
|
this.updateIntersectionObserver();
|
||||||
|
|
||||||
window.registerForActive(this.markNewestFullyVisibleMessageRead);
|
window.registerForActive(this.markNewestFullyVisibleMessageRead);
|
||||||
|
@ -518,7 +504,6 @@ export class Timeline extends React.Component<
|
||||||
|
|
||||||
window.unregisterForActive(this.markNewestFullyVisibleMessageRead);
|
window.unregisterForActive(this.markNewestFullyVisibleMessageRead);
|
||||||
|
|
||||||
this.messagesResizeObserver?.disconnect();
|
|
||||||
this.intersectionObserver?.disconnect();
|
this.intersectionObserver?.disconnect();
|
||||||
|
|
||||||
clearTimeoutIfNecessary(delayedPeekTimeout);
|
clearTimeoutIfNecessary(delayedPeekTimeout);
|
||||||
|
@ -1082,7 +1067,10 @@ export class Timeline extends React.Component<
|
||||||
ref={this.containerRef}
|
ref={this.containerRef}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="module-timeline__messages"
|
className={classNames(
|
||||||
|
'module-timeline__messages',
|
||||||
|
haveNewest && 'module-timeline__messages--have-newest'
|
||||||
|
)}
|
||||||
ref={this.messagesRef}
|
ref={this.messagesRef}
|
||||||
>
|
>
|
||||||
{haveOldest && (
|
{haveOldest && (
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue