diff --git a/app/soapbox/features/chats/components/chat_message_list.js b/app/soapbox/features/chats/components/chat_message_list.js index 2ff423143..1cfc41442 100644 --- a/app/soapbox/features/chats/components/chat_message_list.js +++ b/app/soapbox/features/chats/components/chat_message_list.js @@ -13,8 +13,6 @@ import { escape, throttle } from 'lodash'; import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import Bundle from 'soapbox/features/ui/components/bundle'; -const scrollBottom = (elem) => elem.scrollHeight - elem.offsetHeight - elem.scrollTop; - const makeEmojiMap = record => record.get('emojis', ImmutableList()).reduce((map, emoji) => { return map.set(`:${emoji.get('shortcode')}:`, emoji); }, ImmutableMap()); @@ -45,12 +43,13 @@ class ChatMessageList extends ImmutablePureComponent { } state = { + initialLoad: true, isLoading: false, } scrollToBottom = () => { if (!this.messagesEnd) return; - this.messagesEnd.scrollIntoView(); + this.messagesEnd.scrollIntoView(false); } setMessageEndRef = (el) => { @@ -82,22 +81,45 @@ class ChatMessageList extends ImmutablePureComponent { }); } + isNearBottom = () => { + const elem = this.node; + if (!elem) return false; + + const scrollBottom = elem.scrollHeight - elem.offsetHeight - elem.scrollTop; + return scrollBottom < elem.offsetHeight * 1.5; + } + componentDidMount() { const { dispatch, chatId } = this.props; dispatch(fetchChatMessages(chatId)); this.node.addEventListener('scroll', this.handleScroll); + this.scrollToBottom(); } - componentDidUpdate(prevProps) { + getSnapshotBeforeUpdate(prevProps, prevState) { + const { scrollHeight, scrollTop } = this.node; + return scrollHeight - scrollTop; + } + + restoreScrollPosition = (scrollBottom) => { + this.lastComputedScroll = this.node.scrollHeight - scrollBottom; + this.node.scrollTop = this.lastComputedScroll; + } + + componentDidUpdate(prevProps, prevState, scrollBottom) { + const { initialLoad } = this.state; const oldCount = prevProps.chatMessages.count(); const newCount = this.props.chatMessages.count(); - const isNearBottom = scrollBottom(this.node) < 150; - const historyAdded = prevProps.chatMessages.getIn([-1, 'id']) !== this.props.chatMessages.getIn([-1, 'id']); + const isNearBottom = this.isNearBottom(); + const historyAdded = prevProps.chatMessages.getIn([0, 'id']) !== this.props.chatMessages.getIn([0, 'id']); + + // Retain scroll bar position when loading old messages + this.restoreScrollPosition(scrollBottom); if (oldCount !== newCount) { - if (isNearBottom) this.scrollToBottom(); - if (historyAdded) this.setState({ isLoading: false }); + if (isNearBottom || initialLoad) this.scrollToBottom(); + if (historyAdded) this.setState({ isLoading: false, initialLoad: false }); } } @@ -107,13 +129,20 @@ class ChatMessageList extends ImmutablePureComponent { handleLoadMore = () => { const { dispatch, chatId, chatMessages } = this.props; - const maxId = chatMessages.getIn([-1, 'id']); + const maxId = chatMessages.getIn([0, 'id']); dispatch(fetchChatMessages(chatId, maxId)); this.setState({ isLoading: true }); } handleScroll = throttle(() => { - if (this.node.scrollTop < 150 && !this.state.isLoading) this.handleLoadMore(); + const { lastComputedScroll } = this; + const { isLoading, initialLoad } = this.state; + const { scrollTop, offsetHeight } = this.node; + const computedScroll = lastComputedScroll === scrollTop; + const nearTop = scrollTop < offsetHeight * 2; + + if (nearTop && !isLoading && !initialLoad && !computedScroll) + this.handleLoadMore(); }, 150, { trailing: true, }); @@ -126,15 +155,17 @@ class ChatMessageList extends ImmutablePureComponent { const attachment = chatMessage.get('attachment'); if (!attachment) return null; return ( - - {Component => ( - - )} - +
+ + {Component => ( + + )} + +
); } @@ -159,7 +190,6 @@ class ChatMessageList extends ImmutablePureComponent { return (
-
{chatMessages.map(chatMessage => (
))} +
); } diff --git a/app/soapbox/reducers/chat_message_lists.js b/app/soapbox/reducers/chat_message_lists.js index b584dca72..3e2d1dbae 100644 --- a/app/soapbox/reducers/chat_message_lists.js +++ b/app/soapbox/reducers/chat_message_lists.js @@ -10,8 +10,8 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl const initialState = ImmutableMap(); const idComparator = (a, b) => { - if (a < b) return 1; - if (a > b) return -1; + if (a < b) return -1; + if (a > b) return 1; return 0; }; diff --git a/app/styles/chats.scss b/app/styles/chats.scss index 49719a638..29c2f0c4f 100644 --- a/app/styles/chats.scss +++ b/app/styles/chats.scss @@ -99,18 +99,12 @@ .chat-messages { overflow-y: scroll; flex: 1; - display: flex; - flex-direction: column-reverse; } .chat-message { - padding: 7px 10px; + margin: 14px 10px; display: flex; - &:last-child { - padding-top: 14px; - } - &__bubble { font-size: 15px; padding: 4px 10px; @@ -225,9 +219,10 @@ position: relative; .icon-button { + color: var(--primary-text-color--faint); position: absolute; right: 10px; - top: 8px; + top: calc(50% - 13px); width: auto; height: auto; background: transparent !important; @@ -237,7 +232,7 @@ } .chat-box__send .icon-button { - top: 12px; + top: calc(50% - 9px); } textarea { @@ -347,8 +342,12 @@ } } +.chat-message__media { + height: 120px; +} + .chat-message .media-gallery { - height: auto !important; + height: 100% !important; margin: 4px 0 8px; .spoiler-button { @@ -358,7 +357,7 @@ .media-gallery__item:not(.media-gallery__item--image) { max-width: 100%; width: 120px !important; - height: 70px !important; + height: 100% !important; } &__preview {