keep scrolled position when adding messages at the bottom

pull/1804/head
Audric Ackermann 4 years ago
parent 06dfaa2482
commit 119b6e1baf
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

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

@ -15,7 +15,7 @@ export const GroupInvitation = (props: PropsForGroupInvitation) => {
const openGroupInvitation = window.i18n('openGroupInvitation'); const openGroupInvitation = window.i18n('openGroupInvitation');
return ( 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={classNames(classes)}>
<div className="contents"> <div className="contents">
<SessionIconButton <SessionIconButton

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

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

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

@ -34,7 +34,7 @@ const TimerNotificationContent = (props: PropsForExpirationTimer) => {
export const TimerNotification = (props: PropsForExpirationTimer) => { export const TimerNotification = (props: PropsForExpirationTimer) => {
return ( 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 className="module-timer-notification__message">
<div> <div>
<SessionIcon <SessionIcon

@ -19,7 +19,6 @@ import { ToastUtils, UserUtils } from '../../../session/utils';
import * as MIME from '../../../types/MIME'; import * as MIME from '../../../types/MIME';
import { SessionFileDropzone } from './SessionFileDropzone'; import { SessionFileDropzone } from './SessionFileDropzone';
import { import {
fetchMessagesForConversation,
quoteMessage, quoteMessage,
ReduxConversationType, ReduxConversationType,
resetSelectedMessageIds, resetSelectedMessageIds,
@ -158,7 +157,6 @@ export class SessionConversation extends React.Component<Props, State> {
} }
} }
if (newConversationKey !== oldConversationKey) { if (newConversationKey !== oldConversationKey) {
void this.loadInitialMessages();
this.setState({ this.setState({
showRecordingView: false, showRecordingView: false,
stagedAttachments: [], 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 ~~~~~~~~~~~~ // ~~~~~~~~~~~~ MICROPHONE METHODS ~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@ -1,12 +1,9 @@
import React from 'react'; import React from 'react';
import { SessionScrollButton } from '../SessionScrollButton'; import { SessionScrollButton } from '../SessionScrollButton';
import { Constants } from '../../../session';
import _ from 'lodash'; import _ from 'lodash';
import { contextMenu } from 'react-contexify'; import { contextMenu } from 'react-contexify';
import { import {
fetchMessagesForConversation,
markConversationFullyRead,
quotedMessageToAnimate, quotedMessageToAnimate,
ReduxConversationType, ReduxConversationType,
setNextMessageToPlay, setNextMessageToPlay,
@ -25,14 +22,12 @@ import { ConversationTypeEnum } from '../../../models/conversation';
import { StateType } from '../../../state/reducer'; import { StateType } from '../../../state/reducer';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
areMoreMessagesBeingFetched,
getQuotedMessageToAnimate, getQuotedMessageToAnimate,
getSelectedConversation, getSelectedConversation,
getSelectedConversationKey, getSelectedConversationKey,
getShowScrollButton, getShowScrollButton,
getSortedMessagesOfSelectedConversation, getSortedMessagesOfSelectedConversation,
} from '../../../state/selectors/conversations'; } from '../../../state/selectors/conversations';
import { isElectronWindowFocused } from '../../../session/utils/WindowUtils';
import { SessionMessagesList } from './SessionMessagesList'; import { SessionMessagesList } from './SessionMessagesList';
export type SessionMessageListProps = { export type SessionMessageListProps = {
@ -70,13 +65,8 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
} }
} }
public componentDidUpdate( public componentDidUpdate(prevProps: Props) {
prevProps: Props,
_prevState: any,
snapshot: { scrollHeight: number; scrollTop: number }
) {
const isSameConvo = prevProps.conversationKey === this.props.conversationKey; const isSameConvo = prevProps.conversationKey === this.props.conversationKey;
const messageLengthChanged = prevProps.messagesProps.length !== this.props.messagesProps.length;
if ( if (
!isSameConvo || !isSameConvo ||
(prevProps.messagesProps.length === 0 && this.props.messagesProps.length !== 0) (prevProps.messagesProps.length === 0 && this.props.messagesProps.length !== 0)
@ -85,45 +75,7 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
// displayed conversation changed. We have a bit of cleaning to do here // displayed conversation changed. We have a bit of cleaning to do here
this.initialMessageLoadingPosition(); 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() { public render() {
@ -203,21 +155,33 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
*/ */
private initialMessageLoadingPosition() { private initialMessageLoadingPosition() {
const { messagesProps, conversation } = this.props; const { messagesProps, conversation } = this.props;
if (!conversation) { if (!conversation || !messagesProps.length) {
return; 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 (conversation.unreadCount < messagesProps.length) {
// if we loaded all unread messages, scroll to the first one unread const idToStringTo = messagesProps[conversation.unreadCount - 1].propsForMessage.id;
const firstUnread = Math.max(conversation.unreadCount, 0);
this.scrollToMessage(messagesProps[firstUnread].propsForMessage.id); this.scrollToMessage(idToStringTo, 'end');
} else { } 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); 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) { private scrollToMessage(messageId: string, block: 'center' | 'end' | 'nearest' | 'start') {
const messageElementDom = document.getElementById(`inview-${messageId}`); const messageElementDom = document.getElementById(`msg-${messageId}`);
messageElementDom?.scrollIntoView({ messageElementDom?.scrollIntoView({
behavior: 'auto', behavior: 'auto',
block: 'center', block,
}); });
} }
@ -254,7 +218,6 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
if (!messageContainer) { if (!messageContainer) {
return; return;
} }
console.warn('scrollToBottom on messageslistcontainer');
messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight; messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight;
} }
@ -304,7 +267,7 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
} }
const databaseId = targetMessage.propsForMessage.id; const databaseId = targetMessage.propsForMessage.id;
this.scrollToMessage(databaseId); this.scrollToMessage(databaseId, 'center');
// Highlight this message on the UI // Highlight this message on the UI
window.inboxStore?.dispatch(quotedMessageToAnimate(databaseId)); window.inboxStore?.dispatch(quotedMessageToAnimate(databaseId));
this.setupTimeoutResetQuotedHighlightedMessage(databaseId); this.setupTimeoutResetQuotedHighlightedMessage(databaseId);

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

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

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

Loading…
Cancel
Save