From afe0add9ae2815a23cdc39df720f578340d7349d Mon Sep 17 00:00:00 2001 From: Maksim Varvashevich Date: Sat, 31 Jul 2021 10:35:56 +0300 Subject: [PATCH] fix(#644): fix scrolling issue in message list --- src/base.scss | 1 + .../add-call-modal/add-call-modal.tsx | 8 +- src/components/call-list/call-list.tsx | 8 +- .../chat-media/audio-list/audio-list.tsx | 7 +- .../chat-media/file-list/file-list.tsx | 7 +- .../chat-media/photo-list/photo-list.tsx | 6 +- .../recordings-list/recordings-list.tsx | 7 +- .../chat-media/video-list/video-list.tsx | 7 +- .../chat-members/chat-members.tsx | 6 +- src/components/chat-list/chat-list.tsx | 7 +- .../user-select/user-select.tsx | 7 +- .../forward-modal/forward-modal.tsx | 9 +- src/components/friend-list/friend-list.tsx | 7 +- .../group-chat-add-friend-modal.tsx | 9 +- .../infinite-scroll/infinite-scroll.tsx | 130 ++++++++---------- src/components/message-item/message-item.scss | 28 ++++ src/components/message-list/message-list.tsx | 1 + .../new-chat-modal/new-chat-modal.tsx | 9 +- .../new-message-modal/new-message-modal.tsx | 9 +- src/components/welcome/welcome.scss | 4 + src/hooks/use-intersection-observer.ts | 1 - src/utils/apply-theme.ts | 2 + 22 files changed, 168 insertions(+), 112 deletions(-) diff --git a/src/base.scss b/src/base.scss index d1abcc18a..dee689846 100644 --- a/src/base.scss +++ b/src/base.scss @@ -84,6 +84,7 @@ input { --dt-dark-wt-kingBlue: #262c38; --dt-dark-wt-whiter: #262c38; --dt-dark-wt-kingBlueLight-transparenter: #262c38; + --dt-dark-wt-kingBlueLight-hover: #222732; --dt-dark-wt-kingBlue-lighter: #262c38; --dt-dark-transparent-wt-kingBlueLight: rgba(38, 44, 56, 0.5); --dt-dark-transparent-wt-kingBlueLight-transparent: rgba(38, 44, 56, 0.5); diff --git a/src/components/call-list/add-call/add-call-modal/add-call-modal.tsx b/src/components/call-list/add-call/add-call-modal/add-call-modal.tsx index 46d12416c..19b534c28 100644 --- a/src/components/call-list/add-call/add-call-modal/add-call-modal.tsx +++ b/src/components/call-list/add-call/add-call-modal/add-call-modal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; @@ -24,6 +24,7 @@ interface IAddCallModalProps { export const AddCallModal: React.FC = ({ onClose }) => { const { t } = useTranslation(); + const containerRef = useRef(null); const friendsList = useSelector(getMyFriendsListSelector); const searchFriendsList = useSelector(getMySearchFriendsListSelector); @@ -107,7 +108,7 @@ export const AddCallModal: React.FC = ({ onClose }) => { return ( - <> +
{t('addCallModal.title')} @@ -120,6 +121,7 @@ export const AddCallModal: React.FC = ({ onClose }) => { onChange={handleSearchInputChange} /> = ({ onClose }) => { {selectEntities}
- +
); }; diff --git a/src/components/call-list/call-list.tsx b/src/components/call-list/call-list.tsx index 422e289e7..330dc39d7 100644 --- a/src/components/call-list/call-list.tsx +++ b/src/components/call-list/call-list.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; @@ -18,10 +18,11 @@ const BLOCK_NAME = 'call-list'; export const CallList = () => { const callsList = useSelector(getCallsListSelector); const searchCallsList = useSelector(getSearchCallsListSelector); - const getCalls = useActionWithDispatch(getCallsAction); const resetSearchCalls = useActionWithDispatch(resetSearchCallsAction); + const containerRef = useRef(null); + const [searchString, setSearchString] = useState(''); const changeSearchString = useCallback( (e: React.ChangeEvent) => { @@ -75,8 +76,9 @@ export const CallList = () => { onChange={changeSearchString} /> -
+
diff --git a/src/components/chat-info-right-panel/chat-media/audio-list/audio-list.tsx b/src/components/chat-info-right-panel/chat-media/audio-list/audio-list.tsx index cebe46a7d..cac85f725 100644 --- a/src/components/chat-info-right-panel/chat-media/audio-list/audio-list.tsx +++ b/src/components/chat-info-right-panel/chat-media/audio-list/audio-list.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useRef } from 'react'; import { useSelector } from 'react-redux'; @@ -23,7 +23,7 @@ const AudioAttachmentComponent: React.FC = ({ ...audio }) => ( export const AudioList = () => { const audiosForSelectedChat = useSelector(getSelectedChatAudiosSelector); - + const containerRef = useRef(null); const getAudios = useActionWithDispatch(getAudioAttachmentsAction); const loadMore = useCallback(() => { @@ -39,9 +39,10 @@ export const AudioList = () => { ); return ( -
+
= ({ ...file }) => ( export const FileList = () => { const getRawAttachments = useActionWithDispatch(getRawAttachmentsAction); - + const containerRef = useRef(null); const filesForSelectedChat = useSelector(getSelectedChatFilesSelector); const loadMore = useCallback(() => { @@ -45,8 +45,9 @@ export const FileList = () => { ); return ( -
+
= ({ observeIntersection }) => { const getPhotoAttachments = useActionWithDispatch(getPhotoAttachmentsAction); const photoForSelectedChat = useSelector(getSelectedChatPhotosSelector); + const containerRef = useRef(null); const loadMore = useCallback(() => { const page: IPage = { @@ -54,8 +55,9 @@ const PhotoList: React.FC = ({ observeIntersection }) => { ) : null; return ( -
+
{ const recordingsForSelectedChat = useSelector(getSelectedChatRecordingsSelector); - + const containerRef = useRef(null); const getRecordings = useActionWithDispatch(getVoiceAttachmentsAction); const loadMore = useCallback(() => { @@ -36,9 +36,10 @@ export const RecordingsList = () => { ); return ( -
+
diff --git a/src/components/chat-info-right-panel/chat-media/video-list/video-list.tsx b/src/components/chat-info-right-panel/chat-media/video-list/video-list.tsx index d6e358809..55f0d8021 100644 --- a/src/components/chat-info-right-panel/chat-media/video-list/video-list.tsx +++ b/src/components/chat-info-right-panel/chat-media/video-list/video-list.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useRef } from 'react'; import { useSelector } from 'react-redux'; @@ -19,7 +19,7 @@ import './video-list.scss'; export const VideoList = () => { const getVideoAttachmentss = useActionWithDispatch(getVideoAttachmentsAction); - + const containerRef = useRef(null); const videosForSelectedChat = useSelector(getSelectedChatVideosSelector); const loadMore = useCallback(() => { @@ -45,8 +45,9 @@ export const VideoList = () => { ) : null; return ( -
+
{ const [searchStr, setSearchStr] = useState(''); const [membersDisplayed, setMembersDisplayed] = useState(false); + const containerRef = useRef(null); const { t } = useTranslation(); @@ -64,7 +65,7 @@ export const ChatMembers: React.FC = () => { ); return ( -
+

{t('chatMembers.title')}

{ const chatsList = useSelector(getChatsListSelector); const searchChatsList = useSelector(getSearchChatsListSelector); + const containerRef = useRef(null); + const getChatsRequest = useActionWithDispatch(getChatsAction); const changeSelectedChat = useActionWithDispatch(changeSelectedChatAction); const resetSearchChats = useActionWithDispatch(resetSearchChatsAction); @@ -96,7 +98,7 @@ const ChatList = React.memo(() => { return ( <> -
+
{
diff --git a/src/components/create-group-chat-modal/user-select/user-select.tsx b/src/components/create-group-chat-modal/user-select/user-select.tsx index febb8fccf..777406c9d 100644 --- a/src/components/create-group-chat-modal/user-select/user-select.tsx +++ b/src/components/create-group-chat-modal/user-select/user-select.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useRef } from 'react'; import { useSelector } from 'react-redux'; @@ -21,6 +21,8 @@ const UserSelect: React.FC = ({ changeSelectedState, isSelecte const [name, setName] = useState(''); + const containerRef = useRef(null); + const friendsList = useSelector(getMyFriendsListSelector); const searchFriendsList = useSelector(getMySearchFriendsListSelector); @@ -76,12 +78,13 @@ const UserSelect: React.FC = ({ changeSelectedState, isSelecte }, [name.length, searchFriendIds, friendIds, renderSelectEntity]); return ( -
+
= ({ onClose, messageIdsToForward }) => { const { t } = useTranslation(); + const containerRef = useRef(null); + const chatsList = useSelector(getChatsListSelector); const searchChatsList = useSelector(getSearchChatsListSelector); @@ -115,7 +117,7 @@ export const ForwardModal: React.FC = ({ onClose, messageIds return ( - <> +
<> @@ -130,6 +132,7 @@ export const ForwardModal: React.FC = ({ onClose, messageIds onChange={handleChatSearchChange} /> = ({ onClose, messageIds
- +
); }; diff --git a/src/components/friend-list/friend-list.tsx b/src/components/friend-list/friend-list.tsx index 18f3eed5c..923cd59db 100644 --- a/src/components/friend-list/friend-list.tsx +++ b/src/components/friend-list/friend-list.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; @@ -19,6 +19,8 @@ export const FriendList = () => { const friendsList = useSelector(getMyFriendsListSelector); const searchFriendsList = useSelector(getMySearchFriendsListSelector); + const containerRef = useRef(null); + const { hasMore: hasMoreFriends, friendIds, loading: friendsLoading } = friendsList; const { hasMore: hasMoreSearchFriends, @@ -70,7 +72,7 @@ export const FriendList = () => { }, [searchString.length, searchFriendIds, friendIds, renderFriend]); return ( -
+
{
diff --git a/src/components/group-chat-add-friend-modal/group-chat-add-friend-modal.tsx b/src/components/group-chat-add-friend-modal/group-chat-add-friend-modal.tsx index e666fdd01..0fdc84dc8 100644 --- a/src/components/group-chat-add-friend-modal/group-chat-add-friend-modal.tsx +++ b/src/components/group-chat-add-friend-modal/group-chat-add-friend-modal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; @@ -29,6 +29,8 @@ const BLOCK_NAME = 'group-chat-add-friend-modal'; const GroupChatAddFriendModal: React.FC = ({ onClose }) => { const { t } = useTranslation(); + const containerRef = useRef(null); + const [selectedUserIds, setselectedUserIds] = useState([]); const [loading, setLoading] = useState(false); const [name, setName] = useState(''); @@ -133,7 +135,7 @@ const GroupChatAddFriendModal: React.FC = ({ onCl return ( - <> +
<> @@ -146,6 +148,7 @@ const GroupChatAddFriendModal: React.FC = ({ onCl onChange={handleSearchInputChange} /> = ({ onCl
- +
); }; diff --git a/src/components/infinite-scroll/infinite-scroll.tsx b/src/components/infinite-scroll/infinite-scroll.tsx index cfc30584b..396471ac8 100644 --- a/src/components/infinite-scroll/infinite-scroll.tsx +++ b/src/components/infinite-scroll/infinite-scroll.tsx @@ -1,96 +1,84 @@ -import React from 'react'; +import React, { RefObject, useMemo } from 'react'; -import { InfiniteScrollLoader } from './infinite-scroll-loader/infinite-scroll-loader'; +import classnames from 'classnames'; +import debounce from 'lodash/debounce'; +import noop from 'lodash/noop'; + +import { useIntersectionObserver, useOnIntersect } from '@hooks/use-intersection-observer'; import './infinite-scroll.scss'; type InfiniteScrollProps = { children: React.ReactNode; - Loader?: () => JSX.Element; + loadingIndicator?: () => JSX.Element; className?: string; hasMore?: boolean; isLoading?: boolean; onReachTop?: () => void; onReachBottom?: () => void; threshold?: number | Array; + containerRef: RefObject; }; -export const InfiniteScroll = React.forwardRef( - ( - { - children, - Loader = InfiniteScrollLoader, - className = '', - hasMore, - isLoading, - onReachTop, - onReachBottom, - threshold = 0.0, - }, - ref, - ) => { - const loaderTopRef = React.useRef(null); - const loaderBottomRef = React.useRef(null); - - React.useEffect(() => { - const loadMoreTop = (entries: Array) => { - const [first] = entries; - - if (!isLoading && onReachTop && hasMore && first.isIntersecting) { - onReachTop(); - } - }; +const TRIGGER_MARGIN = 500; - const loadMoreBottom = (entries: Array) => { - const [first] = entries; +const LOAD_MORE_DEBOUNCE = 500; - if (!isLoading && onReachBottom && hasMore && first.isIntersecting) { - onReachBottom(); - } - }; +const BLOCK_NAME = 'infinity-scroll'; - const options = { threshold }; - const topObserver = new IntersectionObserver(loadMoreTop, options); - const bottomObserver = new IntersectionObserver(loadMoreBottom, options); +export const InfiniteScroll: React.FC = ({ + children, + className, + hasMore, + onReachTop = noop, + onReachBottom = noop, + threshold = 0.0, + containerRef, +}) => { + const backwardsTriggerRef = React.useRef(null); + const forwardsTriggerRef = React.useRef(null); - const topLoaderCurrent = loaderTopRef.current; - const bottomLoaderCurrent = loaderBottomRef.current; + const [loadMoreTop, loadMoreBottom] = useMemo( + () => [debounce(onReachTop, LOAD_MORE_DEBOUNCE), debounce(onReachBottom, LOAD_MORE_DEBOUNCE)], + [onReachTop, onReachBottom], + ); - if (topLoaderCurrent) { - topObserver.observe(topLoaderCurrent); + const { observe: observeIntersection } = useIntersectionObserver( + { + rootRef: containerRef, + margin: TRIGGER_MARGIN, + threshold, + }, + (entries) => { + if (!hasMore) { + return; } + const triggerEntry = entries.find(({ isIntersecting }) => isIntersecting); - if (bottomLoaderCurrent) { - bottomObserver.observe(bottomLoaderCurrent); + if (!triggerEntry) { + return; } - return () => { - if (topLoaderCurrent) { - topObserver.unobserve(topLoaderCurrent); - } - - if (bottomLoaderCurrent) { - bottomObserver.unobserve(bottomLoaderCurrent); - } - }; - }, [hasMore, isLoading, loaderBottomRef, loaderTopRef, onReachBottom, onReachTop, threshold]); - - return ( -
- {hasMore && onReachTop && ( -
- -
- )} - {children} - {hasMore && onReachBottom && ( -
- -
- )} -
- ); - }, -); + const { target } = triggerEntry; + + if (target.className === 'top-trigger') { + loadMoreTop(); + } else if (target.className === 'bottom-trigger') { + loadMoreBottom(); + } + }, + ); + + useOnIntersect(forwardsTriggerRef, observeIntersection); + useOnIntersect(backwardsTriggerRef, observeIntersection); + + return ( +
+
+ {children} +
+
+ ); +}; InfiniteScroll.displayName = 'InfiniteScroll'; diff --git a/src/components/message-item/message-item.scss b/src/components/message-item/message-item.scss index aeb0af495..4c9b572d3 100644 --- a/src/components/message-item/message-item.scss +++ b/src/components/message-item/message-item.scss @@ -214,6 +214,8 @@ } &__contents-wrapper { + animation: message 0.15s ease-out 0s forwards; + opacity: 0; display: block; margin-right: 16px; max-width: 70%; @@ -238,3 +240,29 @@ } } } + +@-webkit-keyframes message { + from { + transform-origin: center; + opacity: 0; + transform: scale(0.5); + } + to { + transform-origin: center; + opacity: 1; + transform: scale(1); + } +} + +@keyframes message { + from { + transform-origin: center center; + opacity: 0; + transform: scale(0.8); + } + to { + transform-origin: center center; + opacity: 1; + transform: scale(1); + } +} diff --git a/src/components/message-list/message-list.tsx b/src/components/message-list/message-list.tsx index 86b898c41..5d613789e 100644 --- a/src/components/message-list/message-list.tsx +++ b/src/components/message-list/message-list.tsx @@ -90,6 +90,7 @@ const MessageList = () => { {selectedMessageIds.length > 0 && } = ({ onClose }) => { const [name, setName] = useState(''); + const containerRef = useRef(null); + const friendsList = useSelector(getMyFriendsListSelector); const searchFriendsList = useSelector(getMySearchFriendsListSelector); @@ -104,7 +106,7 @@ const NewChatModal: React.FC = ({ onClose }) => { return ( - <> +
<> @@ -117,6 +119,7 @@ const NewChatModal: React.FC = ({ onClose }) => { onChange={handleSearchInputChange} /> = ({ onClose }) => { {selectEntities}
- +
); }; diff --git a/src/components/new-message-modal/new-message-modal.tsx b/src/components/new-message-modal/new-message-modal.tsx index a5e3c5b8a..442a2c10f 100644 --- a/src/components/new-message-modal/new-message-modal.tsx +++ b/src/components/new-message-modal/new-message-modal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useEffect, useMemo } from 'react'; +import React, { useCallback, useState, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; @@ -33,6 +33,8 @@ const BLOCK_NAME = 'new-message-modal'; const NewMessageModal: React.FC = ({ onClose, displayCreateGroupChat }) => { const { t } = useTranslation(); + const containerRef = useRef(null); + const [name, setName] = useState(''); const friendsList = useSelector(getMyFriendsListSelector); @@ -112,7 +114,7 @@ const NewMessageModal: React.FC = ({ onClose, displayCrea return ( - <> +
<> @@ -135,6 +137,7 @@ const NewMessageModal: React.FC = ({ onClose, displayCrea
= ({ onClose, displayCrea {selectEntities}
- +
); }; diff --git a/src/components/welcome/welcome.scss b/src/components/welcome/welcome.scss index 85887952b..d7eda0d9c 100644 --- a/src/components/welcome/welcome.scss +++ b/src/components/welcome/welcome.scss @@ -40,6 +40,10 @@ &:not(:last-child) { margin-right: 24px; } + + &:hover { + background-color: var(--dt-dark-wt-kingBlueLight-hover); + } } &__option-logo { diff --git a/src/hooks/use-intersection-observer.ts b/src/hooks/use-intersection-observer.ts index db6e6eb6c..58a36b630 100644 --- a/src/hooks/use-intersection-observer.ts +++ b/src/hooks/use-intersection-observer.ts @@ -25,7 +25,6 @@ export function useIntersectionObserver( ) { const intersectionControllerRef = useRef(); const rootCallbackRef = useRef(); - rootCallbackRef.current = rootCallback; useEffect( diff --git a/src/utils/apply-theme.ts b/src/utils/apply-theme.ts index 0c7972973..6fc00e183 100644 --- a/src/utils/apply-theme.ts +++ b/src/utils/apply-theme.ts @@ -49,6 +49,7 @@ export function applyTheme(theme?: Theme) { '--dt-dark-transparent-white-wt-kingBlueLight-transparenter': 'rgba(214, 233, 255, 0.5)', '--dt-dark-wt-kingBlueLight-transparenter': 'rgba(214, 233, 255, 0.5)', + '--dt-dark-wt-kingBlueLight-hover': '#c0d1e5', '--dt-transparent-white-wt-kingBlueLight': 'rgba(63, 138, 224, 0.55)', @@ -95,6 +96,7 @@ export function applyTheme(theme?: Theme) { '--dt-dark-wt-whiter': '#262c38', '--dt-dark-wt-kingBlue-lighter': '#262c38', '--dt-dark-wt-kingBlueLight-transparenter': '#262c38', + '--dt-dark-wt-kingBlueLight-hover': '#222732', '--dt-dark-transparent-wt-kingBlueLight-transparent': 'rgba(38, 44, 56, 0.5)', '--dt-dark-transparent-wt-kingBlueLight': 'rgba(38, 44, 56, 0.5)', '--dt-gray-transparent-wt-kingBlueLight': 'rgba(55, 63, 81, 0.5)',