keep scrolled position when adding messages at the bottom

This commit is contained in:
Audric Ackermann 2021-07-29 17:27:29 +10:00
parent 06dfaa2482
commit 119b6e1baf
No known key found for this signature in database
GPG key ID: 999F434D76324AD4
11 changed files with 50 additions and 109 deletions

View file

@ -23,7 +23,7 @@ export const DataExtractionNotification = (props: PropsForDataExtractionNotifica
flexDirection="column"
alignItems="center"
margin={theme.common.margins.sm}
id={`data-extraction-${messageId}`}
id={`msg-${messageId}`}
>
<SessionIcon
iconType={SessionIconType.Upload}

View file

@ -15,7 +15,7 @@ export const GroupInvitation = (props: PropsForGroupInvitation) => {
const openGroupInvitation = window.i18n('openGroupInvitation');
return (
<div className="group-invitation-container" id={`group-invit-${props.messageId}`}>
<div className="group-invitation-container" id={`msg-${props.messageId}`}>
<div className={classNames(classes)}>
<div className="contents">
<SessionIconButton

View file

@ -91,7 +91,7 @@ function renderChange(change: PropsForGroupUpdateType) {
export const GroupNotification = (props: PropsForGroupUpdate) => {
const { changes } = props;
return (
<div className="module-group-notification" id={`group-notif-${props.messageId}`}>
<div className="module-group-notification" id={`msg-${props.messageId}`}>
{(changes || []).map((change, index) => (
<div key={index} className="module-group-notification__change">
{renderChange(change)}

View file

@ -652,11 +652,13 @@ class MessageInner extends React.PureComponent<Props, State> {
// when the view first loads, it needs to scroll to the unread messages.
// we need to disable the inview on the first loading
if (!this.props.haveDoneFirstScroll) {
console.warn('waiting for first scroll');
if (inView === true) {
window.log.info('onVisible but waiting for first scroll event');
}
return;
}
// we are the bottom message
if (this.props.mostRecentMessageId === messageId) {
if (this.props.mostRecentMessageId === messageId && isElectronWindowFocused()) {
if (inView === true) {
window.inboxStore?.dispatch(showScrollToBottomButton(false));
void getConversationController()
@ -671,12 +673,8 @@ class MessageInner extends React.PureComponent<Props, State> {
window.inboxStore?.dispatch(showScrollToBottomButton(true));
}
}
console.warn('oldestMessageId', this.props.oldestMessageId);
console.warn('mostRecentMessageId', this.props.mostRecentMessageId);
console.warn('messageId', messageId);
if (inView === true && this.props.oldestMessageId === messageId && !fetchingMore) {
console.warn('loadMoreMessages');
if (inView === true && this.props.oldestMessageId === messageId && !fetchingMore) {
this.loadMoreMessages();
}
if (inView === true && shouldMarkReadWhenVisible && isElectronWindowFocused()) {

View file

@ -1,6 +1,6 @@
import React from 'react';
import { useFocus } from '../../hooks/useFocus';
import { InView, useInView } from 'react-intersection-observer';
import { InView } from 'react-intersection-observer';
type ReadableMessageProps = {
children: React.ReactNode;
@ -16,11 +16,11 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
return (
<InView
id={`inview-${messageId}`}
id={`msg-${messageId}`}
{...props}
as="div"
threshold={0.5}
delay={20}
delay={100}
triggerOnce={false}
>
{props.children}

View file

@ -34,7 +34,7 @@ const TimerNotificationContent = (props: PropsForExpirationTimer) => {
export const TimerNotification = (props: PropsForExpirationTimer) => {
return (
<div className="module-timer-notification" id={props.messageId}>
<div className="module-timer-notification" id={`msg-${props.messageId}`}>
<div className="module-timer-notification__message">
<div>
<SessionIcon

View file

@ -19,7 +19,6 @@ import { ToastUtils, UserUtils } from '../../../session/utils';
import * as MIME from '../../../types/MIME';
import { SessionFileDropzone } from './SessionFileDropzone';
import {
fetchMessagesForConversation,
quoteMessage,
ReduxConversationType,
resetSelectedMessageIds,
@ -158,7 +157,6 @@ export class SessionConversation extends React.Component<Props, State> {
}
}
if (newConversationKey !== oldConversationKey) {
void this.loadInitialMessages();
this.setState({
showRecordingView: false,
stagedAttachments: [],
@ -293,26 +291,6 @@ export class SessionConversation extends React.Component<Props, State> {
);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~ GETTER METHODS ~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public async loadInitialMessages() {
const { selectedConversation, selectedConversationKey } = this.props;
if (!selectedConversation) {
return;
}
// lets load only 50 messages and let the user scroll up if he needs more context
(window.inboxStore?.dispatch as any)(
fetchMessagesForConversation({
conversationKey: selectedConversationKey,
count: 30, // first page
})
);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~ MICROPHONE METHODS ~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -1,12 +1,9 @@
import React from 'react';
import { SessionScrollButton } from '../SessionScrollButton';
import { Constants } from '../../../session';
import _ from 'lodash';
import { contextMenu } from 'react-contexify';
import {
fetchMessagesForConversation,
markConversationFullyRead,
quotedMessageToAnimate,
ReduxConversationType,
setNextMessageToPlay,
@ -25,14 +22,12 @@ import { ConversationTypeEnum } from '../../../models/conversation';
import { StateType } from '../../../state/reducer';
import { connect } from 'react-redux';
import {
areMoreMessagesBeingFetched,
getQuotedMessageToAnimate,
getSelectedConversation,
getSelectedConversationKey,
getShowScrollButton,
getSortedMessagesOfSelectedConversation,
} from '../../../state/selectors/conversations';
import { isElectronWindowFocused } from '../../../session/utils/WindowUtils';
import { SessionMessagesList } from './SessionMessagesList';
export type SessionMessageListProps = {
@ -70,13 +65,8 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
}
}
public componentDidUpdate(
prevProps: Props,
_prevState: any,
snapshot: { scrollHeight: number; scrollTop: number }
) {
public componentDidUpdate(prevProps: Props) {
const isSameConvo = prevProps.conversationKey === this.props.conversationKey;
const messageLengthChanged = prevProps.messagesProps.length !== this.props.messagesProps.length;
if (
!isSameConvo ||
(prevProps.messagesProps.length === 0 && this.props.messagesProps.length !== 0)
@ -85,47 +75,9 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
// displayed conversation changed. We have a bit of cleaning to do here
this.initialMessageLoadingPosition();
} else {
// if we got new message for this convo, and we are scrolled to bottom
if (isSameConvo && messageLengthChanged) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (prevProps.messagesProps.length && snapshot !== null) {
const list = this.props.messageContainerRef.current;
// if we added a message at the top, keep position from the bottom.
if (
prevProps.messagesProps[0].propsForMessage.id ===
this.props.messagesProps[0].propsForMessage.id
) {
list.scrollTop = list.scrollHeight - (snapshot.scrollHeight - snapshot.scrollTop);
} else {
// if we added a message at the bottom, keep position from the bottom.
list.scrollTop = snapshot.scrollTop;
}
}
}
}
}
public getSnapshotBeforeUpdate(prevProps: Props) {
// getSnapshotBeforeUpdate is kind of pain to do in react hooks, so better keep the message list as a
// class component for now
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.messagesProps.length < this.props.messagesProps.length) {
const list = this.props.messageContainerRef.current;
console.warn('getSnapshotBeforeUpdate ', {
scrollHeight: list.scrollHeight,
scrollTop: list.scrollTop,
});
return { scrollHeight: list.scrollHeight, scrollTop: list.scrollTop };
}
return null;
}
public render() {
const { conversationKey, conversation } = this.props;
@ -203,21 +155,33 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
*/
private initialMessageLoadingPosition() {
const { messagesProps, conversation } = this.props;
if (!conversation) {
if (!conversation || !messagesProps.length) {
return;
}
if (conversation.unreadCount > 0 && messagesProps.length) {
if (conversation.unreadCount <= 0) {
this.scrollToBottom();
} else {
// just assume that this need to be shown by default
window.inboxStore?.dispatch(showScrollToBottomButton(true));
// conversation.unreadCount > 0
// either we loaded all unread messages or not
if (conversation.unreadCount < messagesProps.length) {
// if we loaded all unread messages, scroll to the first one unread
const firstUnread = Math.max(conversation.unreadCount, 0);
this.scrollToMessage(messagesProps[firstUnread].propsForMessage.id);
const idToStringTo = messagesProps[conversation.unreadCount - 1].propsForMessage.id;
this.scrollToMessage(idToStringTo, 'end');
} else {
// if we did not load all unread messages, just scroll to the middle of the loaded messages list. so the user can choose to go up or down from there
// just scroll to the middle as we don't have enough loaded message nevertheless
const middle = Math.floor(messagesProps.length / 2);
this.scrollToMessage(messagesProps[middle].propsForMessage.id);
const idToStringTo = messagesProps[middle].propsForMessage.id;
this.scrollToMessage(idToStringTo, 'center');
}
}
// window.inboxStore?.dispatch(updateHaveDoneFirstScroll());
setTimeout(() => {
window.inboxStore?.dispatch(updateHaveDoneFirstScroll());
}, 100);
}
/**
@ -241,11 +205,11 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
}
}
private scrollToMessage(messageId: string) {
const messageElementDom = document.getElementById(`inview-${messageId}`);
private scrollToMessage(messageId: string, block: 'center' | 'end' | 'nearest' | 'start') {
const messageElementDom = document.getElementById(`msg-${messageId}`);
messageElementDom?.scrollIntoView({
behavior: 'auto',
block: 'center',
block,
});
}
@ -254,7 +218,6 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
if (!messageContainer) {
return;
}
console.warn('scrollToBottom on messageslistcontainer');
messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight;
}
@ -304,7 +267,7 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
}
const databaseId = targetMessage.propsForMessage.id;
this.scrollToMessage(databaseId);
this.scrollToMessage(databaseId, 'center');
// Highlight this message on the UI
window.inboxStore?.dispatch(quotedMessageToAnimate(databaseId));
this.setupTimeoutResetQuotedHighlightedMessage(databaseId);

View file

@ -190,7 +190,6 @@ export const sendViaOnion = async (
);
} catch (e) {
window?.log?.warn('sendViaOnionRetryable failed ', e);
// console.warn('error to show to user', e);
return null;
}

View file

@ -787,6 +787,7 @@ export async function openConversationWithMessages(args: {
const { conversationKey, messageId } = args;
const firstUnreadIdOnOpen = await getFirstUnreadMessageIdInConversation(conversationKey);
// preload 30 messages
const initialMessages = await getMessages(conversationKey, 30);
window.inboxStore?.dispatch(

View file

@ -415,19 +415,21 @@ export const getFirstUnreadMessageId = createSelector(
);
export const getMostRecentMessageId = createSelector(
getConversations,
(state: ConversationsStateType): string | undefined => {
return state.messages.length ? state.messages[0].propsForMessage.id : undefined;
getSortedMessagesOfSelectedConversation,
(messages: Array<MessageModelProps>): string | undefined => {
return messages.length ? messages[0].propsForMessage.id : undefined;
}
);
export const getOldestMessageId = createSelector(getConversations, (state: ConversationsStateType):
| string
| undefined => {
return state.messages.length
? state.messages[state.messages.length - 1].propsForMessage.id
: undefined;
});
export const getOldestMessageId = createSelector(
getSortedMessagesOfSelectedConversation,
(messages: Array<MessageModelProps>): string | undefined => {
const oldest =
messages.length > 0 ? messages[messages.length - 1].propsForMessage.id : undefined;
return oldest;
}
);
export const getLoadedMessagesLength = createSelector(
getConversations,