Skip to content

Commit

Permalink
Merge branch 'dev' of github.com:wireapp/wire-webapp into new-navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
thisisamir98 committed Feb 19, 2024
2 parents 0cea6ab + 70c584f commit 22d9b92
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 93 deletions.
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"date-fns": "3.3.1",
"dexie-batch": "0.4.3",
"dexie-encrypted": "2.0.0",
"emoji-picker-react": "4.7.17",
"emoji-picker-react": "4.7.18",
"highlight.js": "11.9.0",
"http-status-codes": "2.3.0",
"jimp": "0.22.10",
Expand All @@ -43,8 +43,8 @@
"react-error-boundary": "4.0.12",
"react-intl": "6.6.2",
"react-redux": "9.1.0",
"react-router": "6.22.0",
"react-router-dom": "6.22.0",
"react-router": "6.22.1",
"react-router-dom": "6.22.1",
"react-transition-group": "4.4.5",
"redux": "5.0.1",
"redux-logdown": "1.0.4",
Expand All @@ -55,7 +55,7 @@
"underscore": "1.13.6",
"uuidjs": "4.2.13",
"webrtc-adapter": "8.2.3",
"zustand": "4.5.0"
"zustand": "4.5.1"
},
"devDependencies": {
"@babel/core": "7.23.9",
Expand All @@ -81,7 +81,7 @@
"@types/linkify-it": "3.0.5",
"@types/loadable__component": "5.13.8",
"@types/markdown-it": "13.0.7",
"@types/node": "^20.11.17",
"@types/node": "^20.11.19",
"@types/open-graph": "0.2.5",
"@types/platform": "1.3.6",
"@types/react": "18.2.55",
Expand Down Expand Up @@ -126,7 +126,7 @@
"os-browserify": "0.3.0",
"path-browserify": "1.0.1",
"postcss": "8.4.35",
"postcss-import": "^16.0.0",
"postcss-import": "^16.0.1",
"postcss-less": "6.0.0",
"postcss-loader": "^8.1.0",
"postcss-preset-env": "^9.3.0",
Expand All @@ -145,7 +145,7 @@
"ts-node": "10.9.2",
"tsc-watch": "6.0.4",
"typescript": "5.3.3",
"webpack": "5.90.1",
"webpack": "5.90.2",
"webpack-cli": "5.1.4",
"webpack-dev-middleware": "7.0.0",
"webpack-hot-middleware": "2.26.1",
Expand Down
65 changes: 21 additions & 44 deletions src/script/components/MessagesList/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*
*/

import React, {FC, useEffect, useLayoutEffect, useRef, useState} from 'react';
import React, {FC, useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';

import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums';
import cx from 'classnames';
Expand All @@ -41,12 +41,10 @@ import {Message, MessageActions} from './Message';
import {MarkerComponent} from './Message/Marker';
import {ScrollToElement} from './Message/types';
import {groupMessagesBySenderAndTime, isMarker} from './utils/messagesGroup';
import {updateScroll, FocusedElement} from './utils/scrollUpdater';

import {Conversation as ConversationEntity, Conversation} from '../../entity/Conversation';
import {isContentMessage} from '../../guards/Message';
import {StatusType} from '../../message/StatusType';

type FocusedElement = {center?: boolean; element: Element};

interface MessagesListParams {
cancelConnectionRequest: (message: MemberMessage) => void;
Expand Down Expand Up @@ -152,45 +150,30 @@ export const MessagesList: FC<MessagesListParams> = ({
const nbMessages = useRef(0);
const focusedElement = useRef<FocusedElement | null>(null);

const updateScroll = (container: Element | null) => {
const scrollingContainer = container?.parentElement;

const syncScrollPosition = useCallback(() => {
const scrollingContainer = messagesContainer?.parentElement;
if (!scrollingContainer || !loaded) {
return;
}

const lastMessage = filteredMessages[filteredMessagesLength - 1];
const previousScrollHeight = scrollHeight.current;
const scrollBottomPosition = scrollingContainer.scrollTop + scrollingContainer.clientHeight;
const shouldStickToBottom = previousScrollHeight - scrollBottomPosition < 100;

if (focusedElement.current) {
// If we have an element we want to focus
const {element, center} = focusedElement.current;
const elementPosition = element.getBoundingClientRect();
const containerPosition = scrollingContainer.getBoundingClientRect();
const scrollBy = scrollingContainer.scrollTop + elementPosition.top - containerPosition.top;
scrollingContainer.scrollTo?.({top: scrollBy - (center ? scrollingContainer.offsetHeight / 2 : 0)});
} else if (scrollingContainer.scrollTop === 0 && scrollingContainer.scrollHeight > previousScrollHeight) {
// If we hit the top and new messages were loaded, we keep the scroll position stable
scrollingContainer.scrollTop = scrollingContainer.scrollHeight - previousScrollHeight;
} else if (shouldStickToBottom) {
// We only want to animate the scroll if there are new messages in the list
const behavior = nbMessages.current !== filteredMessagesLength ? 'smooth' : 'auto';
// Simple content update, we just scroll to bottom if we are in the stick to bottom threshold
scrollingContainer.scrollTo?.({behavior, top: scrollingContainer.scrollHeight});
} else if (lastMessage && lastMessage.status() === StatusType.SENDING && lastMessage.user().id === selfUser.id) {
// The self user just sent a message, we scroll straight to the bottom
scrollingContainer.scrollTo?.({behavior: 'smooth', top: scrollingContainer.scrollHeight});
}
scrollHeight.current = scrollingContainer.scrollHeight;
nbMessages.current = filteredMessagesLength;
};
const newScrollHeight = updateScroll(scrollingContainer, {
focusedElement: focusedElement.current,
prevScrollHeight: scrollHeight.current,
prevNbMessages: nbMessages.current,
messages: filteredMessages,
selfUserId: selfUser?.id,
});

// Listen to resizes of the the container element (if it's resized it means something has changed in the message list)
useResizeObserver(() => updateScroll(messagesContainer), messagesContainer);
nbMessages.current = filteredMessages.length;
scrollHeight.current = newScrollHeight;
}, [messagesContainer?.parentElement, loaded, filteredMessages, selfUser?.id]);

// Listen to resizes of the the content element (if it's resized it means something has changed in the message list, link a link preview was generated)
useResizeObserver(syncScrollPosition, messagesContainer);
// Also listen to the scrolling container resizes (when the window resizes or the inputBar changes)
useResizeObserver(() => updateScroll(messagesContainer), messagesContainer?.parentElement);
useResizeObserver(syncScrollPosition, messagesContainer?.parentElement);

useLayoutEffect(syncScrollPosition, [syncScrollPosition]);

const loadPrecedingMessages = async (): Promise<void> => {
const shouldPullMessages = !isPending && hasAdditionalMessages;
Expand All @@ -213,12 +196,6 @@ export const MessagesList: FC<MessagesListParams> = ({
}
};

useLayoutEffect(() => {
if (messagesContainer) {
updateScroll(messagesContainer);
}
}, [messagesContainer, filteredMessagesLength]);

useEffect(() => {
onLoading(true);
setLoaded(false);
Expand Down Expand Up @@ -265,7 +242,7 @@ export const MessagesList: FC<MessagesListParams> = ({
}
focusedElement.current = {center, element};
setTimeout(() => (focusedElement.current = null), 1000);
updateScroll(messagesContainer);
syncScrollPosition();
};

return (
Expand Down
163 changes: 163 additions & 0 deletions src/script/components/MessagesList/utils/scrollUpdater.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {Message} from 'src/script/entity/message/Message';
import {User} from 'src/script/entity/User';
import {StatusType} from 'src/script/message/StatusType';

import {updateScroll} from './scrollUpdater';

const createScrollingContainer = (height: number, contentHeight: number, scrollTop: number) => {
const container = document.createElement('div');
Object.defineProperty(container, 'clientHeight', {configurable: true, value: height});
Object.defineProperty(container, 'scrollHeight', {configurable: true, value: contentHeight});
container.scrollTop = scrollTop;
container.scrollTo = jest.fn();
return container;
};

const stickToBottomThreshold = 100;

describe('updateScroll', () => {
it('should go to the bottom when the content is first loaded', () => {
const messages = [new Message()];
// container was empty
const prevScrollHeight = 0;
const prevNbMessages = 0;
const selfUserId = 'user1';

const container = createScrollingContainer(100, 500, 0);

updateScroll(container, {
focusedElement: null,
prevScrollHeight,
prevNbMessages,
messages,
selfUserId,
});

// container should be scrolled to the bottom
expect(container.scrollTop).toBe(500);
});

it(`should smoothly stick to the bottom if we are under the ${stickToBottomThreshold}px threshold`, () => {
// container was empty
const prevScrollHeight = 0;
const prevNbMessages = 0;
const selfUserId = 'user1';

const container = createScrollingContainer(100, 500, 500 - stickToBottomThreshold + 1);

updateScroll(container, {
focusedElement: null,
prevScrollHeight,
prevNbMessages,
messages: [new Message()],
selfUserId,
});

// container should be scrolled to the bottom
expect(container.scrollTo).toHaveBeenCalledWith({behavior: 'smooth', top: 500});
});

it(`should stick to the bottom without animation if we are under the ${stickToBottomThreshold}px threshold and no new messages arrive`, () => {
// container was empty
const prevScrollHeight = 0;
const prevNbMessages = 0;
const selfUserId = 'user1';

const container = createScrollingContainer(100, 500, 500 - stickToBottomThreshold + 1);

updateScroll(container, {
focusedElement: null,
prevScrollHeight,
prevNbMessages,
messages: [],
selfUserId,
});

// container should be scrolled to the bottom
expect(container.scrollTo).toHaveBeenCalledWith({behavior: 'auto', top: 500});
});

it(`should not stick to the bottom if we are over the ${stickToBottomThreshold}px threshold`, () => {
const prevScrollHeight = 500 - stickToBottomThreshold - 1;
const prevNbMessages = 0;
const selfUserId = 'user1';

const container = createScrollingContainer(100, 500, 100);

updateScroll(container, {
focusedElement: null,
prevScrollHeight,
prevNbMessages,
messages: [],
selfUserId,
});

// container should be scrolled to the bottom
expect(container.scrollTo).not.toHaveBeenCalled();
expect(container.scrollTop).toBe(100);
});

it('should keep the scroll untouched if we loaded new messages when hitting the top', () => {
const prevScrollHeight = 500;
const prevNbMessages = 0;
const selfUserId = 'user1';
const newMessageHeight = 200;

const container = createScrollingContainer(100, prevScrollHeight + newMessageHeight, 0);

updateScroll(container, {
focusedElement: null,
prevScrollHeight,
prevNbMessages,
messages: [new Message()],
selfUserId,
});

// container should be scrolled to the bottom
expect(container.scrollTo).not.toHaveBeenCalled();
expect(container.scrollTop).toBe(newMessageHeight);
});

it('should smoothly scroll to the last sent message from the self user', () => {
const prevScrollHeight = 500;
const prevNbMessages = 0;
const selfUserId = 'user1';
const newMessageHeight = 200;

const container = createScrollingContainer(100, prevScrollHeight + newMessageHeight, 10);

const newMessage = new Message();
newMessage.user(new User(selfUserId));
newMessage.status(StatusType.SENDING);

updateScroll(container, {
focusedElement: null,
prevScrollHeight,
prevNbMessages,
messages: [newMessage],
selfUserId,
});

// container should be scrolled to the bottom
expect(container.scrollTo).toHaveBeenCalledWith({behavior: 'smooth', top: container.scrollHeight});
});
});
66 changes: 66 additions & 0 deletions src/script/components/MessagesList/utils/scrollUpdater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {Message} from 'src/script/entity/message/Message';
import {StatusType} from 'src/script/message/StatusType';

export type FocusedElement = {center?: boolean; element: Element};

type MessageListContext = {
focusedElement: FocusedElement | null;
prevScrollHeight: number;
prevNbMessages: number;
messages: Message[];
selfUserId: string;
};

export function updateScroll(
container: HTMLElement,
{focusedElement, prevScrollHeight, prevNbMessages, messages, selfUserId}: MessageListContext,
) {
const newNbMessages = messages.length;
const lastMessage = messages[newNbMessages - 1];
const scrollBottomPosition = container.scrollTop + container.clientHeight;
const shouldStickToBottom = prevScrollHeight - scrollBottomPosition < 100;

if (focusedElement) {
// If we have an element we want to focus
const {element, center} = focusedElement;
const elementPosition = element.getBoundingClientRect();
const containerPosition = container.getBoundingClientRect();
const scrollBy = container.scrollTop + elementPosition.top - containerPosition.top;
container.scrollTo?.({top: scrollBy - (center ? container.offsetHeight / 2 : 0)});
} else if (container.scrollTop === 0 && container.scrollHeight > prevScrollHeight) {
// If we hit the top and new messages were loaded, we keep the scroll position stable
container.scrollTop = container.scrollHeight - prevScrollHeight;
} else if (shouldStickToBottom) {
// We only want to animate the scroll if there are new messages in the list
const nbNewMessages = newNbMessages - prevNbMessages;
if (nbNewMessages <= 1) {
// We only want to animate the scroll if there is a single new message (many messages added at once means we are navigating the messages list)
const behavior = prevNbMessages !== newNbMessages ? 'smooth' : 'auto';
// Simple content update, we just scroll to bottom if we are in the stick to bottom threshold
container.scrollTo?.({behavior, top: container.scrollHeight});
}
} else if (lastMessage && lastMessage.status() === StatusType.SENDING && lastMessage.user().id === selfUserId) {
// The self user just sent a message, we scroll straight to the bottom
container.scrollTo?.({behavior: 'smooth', top: container.scrollHeight});
}
return container.scrollHeight;
}
Loading

0 comments on commit 22d9b92

Please sign in to comment.