-
Notifications
You must be signed in to change notification settings - Fork 290
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'dev' of github.com:wireapp/wire-webapp into new-navigation
- Loading branch information
Showing
7 changed files
with
310 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 163 additions & 0 deletions
163
src/script/components/MessagesList/utils/scrollUpdater.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.