From 119b6e1baf953d8493725831e8c518f9b9d619f7 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 29 Jul 2021 17:27:29 +1000 Subject: [PATCH] keep scrolled position when adding messages at the bottom --- .../DataExtractionNotification.tsx | 2 +- .../conversation/GroupInvitation.tsx | 2 +- .../conversation/GroupNotification.tsx | 2 +- ts/components/conversation/Message.tsx | 12 ++- .../conversation/ReadableMessage.tsx | 6 +- .../conversation/TimerNotification.tsx | 2 +- .../conversation/SessionConversation.tsx | 22 ----- .../SessionMessagesListContainer.tsx | 87 ++++++------------- ts/session/onions/onionSend.ts | 1 - ts/state/ducks/conversations.ts | 1 + ts/state/selectors/conversations.ts | 22 ++--- 11 files changed, 50 insertions(+), 109 deletions(-) diff --git a/ts/components/conversation/DataExtractionNotification.tsx b/ts/components/conversation/DataExtractionNotification.tsx index fde513c1e..5eea9cedd 100644 --- a/ts/components/conversation/DataExtractionNotification.tsx +++ b/ts/components/conversation/DataExtractionNotification.tsx @@ -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}`} > { const openGroupInvitation = window.i18n('openGroupInvitation'); return ( -
+
{ const { changes } = props; return ( -
+
{(changes || []).map((change, index) => (
{renderChange(change)} diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index c8abbee3f..dff9b68d6 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -652,11 +652,13 @@ class MessageInner extends React.PureComponent { // 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 { 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()) { diff --git a/ts/components/conversation/ReadableMessage.tsx b/ts/components/conversation/ReadableMessage.tsx index bcd845261..d2c2e2c1e 100644 --- a/ts/components/conversation/ReadableMessage.tsx +++ b/ts/components/conversation/ReadableMessage.tsx @@ -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 ( {props.children} diff --git a/ts/components/conversation/TimerNotification.tsx b/ts/components/conversation/TimerNotification.tsx index ac6ef8bed..7c5cb5d08 100644 --- a/ts/components/conversation/TimerNotification.tsx +++ b/ts/components/conversation/TimerNotification.tsx @@ -34,7 +34,7 @@ const TimerNotificationContent = (props: PropsForExpirationTimer) => { export const TimerNotification = (props: PropsForExpirationTimer) => { return ( -
+
{ } } if (newConversationKey !== oldConversationKey) { - void this.loadInitialMessages(); this.setState({ showRecordingView: false, stagedAttachments: [], @@ -293,26 +291,6 @@ export class SessionConversation extends React.Component { ); } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // ~~~~~~~~~~~~~~ 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 ~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/ts/components/session/conversation/SessionMessagesListContainer.tsx b/ts/components/session/conversation/SessionMessagesListContainer.tsx index dd3c3ba0c..15cb787c1 100644 --- a/ts/components/session/conversation/SessionMessagesListContainer.tsx +++ b/ts/components/session/conversation/SessionMessagesListContainer.tsx @@ -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 { } } - 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,45 +75,7 @@ class SessionMessagesListContainerInner extends React.Component { // 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() { @@ -203,21 +155,33 @@ class SessionMessagesListContainerInner extends React.Component { */ 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 { } } - 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 { if (!messageContainer) { return; } - console.warn('scrollToBottom on messageslistcontainer'); messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight; } @@ -304,7 +267,7 @@ class SessionMessagesListContainerInner extends React.Component { } 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); diff --git a/ts/session/onions/onionSend.ts b/ts/session/onions/onionSend.ts index 785d369d4..bec9304c5 100644 --- a/ts/session/onions/onionSend.ts +++ b/ts/session/onions/onionSend.ts @@ -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; } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index cf1dda40a..07af3facb 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -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( diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 20792deec..763be4bd6 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -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): 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): string | undefined => { + const oldest = + messages.length > 0 ? messages[messages.length - 1].propsForMessage.id : undefined; + + return oldest; + } +); export const getLoadedMessagesLength = createSelector( getConversations,