diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 43060d742..d32e97a66 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -7,8 +7,7 @@ import { ApiMessage, ApiRestrictionReason, MAIN_THREAD_ID } from '../../api/type import { GlobalActions, MessageListType } from '../../global/types'; import { LoadMoreDirection } from '../../types'; -import { ANIMATION_END_DELAY, MESSAGE_LIST_SLICE, SCHEDULED_WHEN_ONLINE } from '../../config'; -import { IS_ANDROID, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; +import { ANIMATION_END_DELAY, MESSAGE_LIST_SLICE } from '../../config'; import { selectChatMessages, selectIsViewportNewest, @@ -25,42 +24,24 @@ import { selectScheduledMessages, selectCurrentMessageIds, } from '../../modules/selectors'; -import { - getMessageOriginalId, - isActionMessage, - isChatChannel, - isChatPrivate, - isOwnMessage, -} from '../../modules/helpers'; -import { - compact, - flatten, - orderBy, - pick, -} from '../../util/iteratees'; -import { - fastRaf, debounce, onTickEnd, -} from '../../util/schedulers'; -import { formatHumanDate } from '../../util/dateFormat'; +import { isChatChannel, isChatPrivate } from '../../modules/helpers'; +import { orderBy, pick } from '../../util/iteratees'; +import { fastRaf, debounce, onTickEnd } from '../../util/schedulers'; import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps'; import buildClassName from '../../util/buildClassName'; -import { groupMessages, MessageDateGroup, isAlbum } from './helpers/groupMessages'; +import { groupMessages } from './helpers/groupMessages'; import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; -import { ObserveFn, useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import useOnChange from '../../hooks/useOnChange'; import useStickyDates from './hooks/useStickyDates'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import resetScroll from '../../util/resetScroll'; import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll'; import renderText from '../common/helpers/renderText'; -import useLang, { LangFn } from '../../hooks/useLang'; +import useLang from '../../hooks/useLang'; import useWindowSize from '../../hooks/useWindowSize'; -import useBackgroundMode from '../../hooks/useBackgroundMode'; import Loading from '../ui/Loading'; -import MessageScroll from './MessageScroll'; -import Message from './message/Message'; -import ActionMessage from './ActionMessage'; +import MessageListContent from './MessageListContent'; import './MessageList.scss'; @@ -96,16 +77,12 @@ type StateProps = { hasLinkedChat?: boolean; }; -type DispatchProps = Pick; +type DispatchProps = Pick; const BOTTOM_THRESHOLD = 100; const UNREAD_DIVIDER_TOP = 10; const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60; const SCROLL_DEBOUNCE = 200; -const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350; -const INTERSECTION_MARGIN_FOR_MEDIA = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500; const FOCUSING_DURATION = 1000; const BOTTOM_FOCUS_MARGIN = 20; const SELECT_MODE_ANIMATION_DURATION = 200; @@ -137,8 +114,6 @@ const MessageList: FC = ({ isSelectModeActive, animationLevel, loadViewportMessages, - markMessageListRead, - markMessagesRead, setScrollOffset, lastMessage, botDescription, @@ -184,65 +159,10 @@ const MessageList: FC = ({ } }, [firstUnreadId]); - const { - observe: observeIntersectionForMedia, - } = useIntersectionObserver({ - rootRef: containerRef, - throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA, - margin: INTERSECTION_MARGIN_FOR_MEDIA, - }); - - const { - observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading, - } = useIntersectionObserver({ - rootRef: containerRef, - }, (entries) => { - if (type !== 'thread') { - return; - } - - let maxId = 0; - const mentionIds: number[] = []; - - entries.forEach((entry) => { - const { isIntersecting, target } = entry; - - if (!isIntersecting) { - return; - } - - const { dataset } = target as HTMLDivElement; - - const messageId = Number(dataset.lastMessageId || dataset.messageId); - if (messageId > maxId) { - maxId = messageId; - } - - if (dataset.hasUnreadMention) { - mentionIds.push(messageId); - } - }); - - if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) { - markMessageListRead({ maxId }); - } - - if (mentionIds.length) { - markMessagesRead({ messageIds: mentionIds }); - } - }); - - useBackgroundMode(freezeForReading, unfreezeForReading); - useOnChange(() => { memoFocusingIdRef.current = focusingId; }, [focusingId]); - const { observe: observeIntersectionForAnimatedStickers } = useIntersectionObserver({ - rootRef: containerRef, - throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA, - }); - const messageGroups = useMemo(() => { if (!messageIds || !messagesById) { return undefined; @@ -260,15 +180,14 @@ const MessageList: FC = ({ return groupMessages(orderBy(listedMessages, ['date', 'id']), memoUnreadDividerBeforeIdRef.current); }, [messageIds, messagesById, threadFirstMessageId, threadTopMessageId]); - const [loadMoreBackwards, loadMoreForwards, loadMoreAround] = useMemo( - () => (type === 'thread' ? [ - debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Backwards }), 1000, true, false), - debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Forwards }), 1000, true, false), - debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Around }), 1000, true, false), - ] : []), + const loadMoreAround = useMemo(() => { + if (type !== 'thread') { + return undefined; + } + + return debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Around }), 1000, true, false); // eslint-disable-next-line react-hooks/exhaustive-deps - [loadViewportMessages, messageIds], - ); + }, [loadViewportMessages, messageIds]); const { isScrolled, updateStickyDates } = useStickyDates(); @@ -535,37 +454,26 @@ const MessageList: FC = ({ ) : messageIds && !messageGroups ? (
{lang('NoMessages')}
) : ((messageIds && messageGroups) || lastMessage) ? ( - - {renderMessages( - lang, - messageGroups || groupMessages([lastMessage!]), - observeIntersectionForReading, - observeIntersectionForMedia, - observeIntersectionForAnimatedStickers, - withUsers, - noAvatars, - anchorIdRef, - memoUnreadDividerBeforeIdRef, - threadId, - type, - threadTopMessageId, - threadFirstMessageId, - hasLinkedChat, - messageGroups ? type === 'scheduled' : false, - !messageGroups || !shouldAnimateAppearanceRef.current, - openHistoryCalendar, - )} - + openHistoryCalendar={openHistoryCalendar} + /> ) : ( )} @@ -573,166 +481,6 @@ const MessageList: FC = ({ ); }; -function renderMessages( - lang: LangFn, - messageGroups: MessageDateGroup[], - observeIntersectionForReading: ObserveFn, - observeIntersectionForMedia: ObserveFn, - observeIntersectionForAnimatedStickers: ObserveFn, - withUsers: boolean, - noAvatars: boolean, - currentAnchorIdRef: { current: string | undefined }, - memoFirstUnreadIdRef: { current: number | undefined }, - threadId: number, - type: MessageListType, - threadTopMessageId: number | undefined, - threadFirstMessageId: number | undefined, - hasLinkedChat: boolean | undefined, - isSchedule: boolean, - noAppearanceAnimation: boolean, - openHistoryCalendar: Function, -) { - const unreadDivider = ( -
- {lang('UnreadMessages')} -
- ); - - const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => { - return acc + flatten(messageGroup.senderGroups).length; - }, 0); - let appearanceIndex = 0; - - const dateGroups = messageGroups.map(( - dateGroup: MessageDateGroup, - dateGroupIndex: number, - dateGroupsArray: MessageDateGroup[], - ) => { - const senderGroups = dateGroup.senderGroups.map(( - senderGroup, - senderGroupIndex, - senderGroupsArray, - ) => { - if (senderGroup.length === 1 && !isAlbum(senderGroup[0]) && isActionMessage(senderGroup[0])) { - const message = senderGroup[0]; - const isLastInList = ( - senderGroupIndex === senderGroupsArray.length - 1 - && dateGroupIndex === dateGroupsArray.length - 1 - ); - - return compact([ - message.id === memoFirstUnreadIdRef.current && unreadDivider, - , - ]); - } - - let currentDocumentGroupId: string | undefined; - - return flatten(senderGroup.map(( - messageOrAlbum, - messageIndex, - ) => { - const message = isAlbum(messageOrAlbum) ? messageOrAlbum.mainMessage : messageOrAlbum; - const album = isAlbum(messageOrAlbum) ? messageOrAlbum : undefined; - const isOwn = isOwnMessage(message); - const isMessageAlbum = isAlbum(messageOrAlbum); - const nextMessage = senderGroup[messageIndex + 1]; - - if (message.previousLocalId && currentAnchorIdRef.current === `message${message.previousLocalId}`) { - currentAnchorIdRef.current = `message${message.id}`; - } - - const documentGroupId = !isMessageAlbum && message.groupedId ? message.groupedId : undefined; - const nextDocumentGroupId = nextMessage && !isAlbum(nextMessage) ? nextMessage.groupedId : undefined; - - const position = { - isFirstInGroup: messageIndex === 0, - isLastInGroup: messageIndex === senderGroup.length - 1, - isFirstInDocumentGroup: Boolean(documentGroupId && documentGroupId !== currentDocumentGroupId), - isLastInDocumentGroup: Boolean(documentGroupId && documentGroupId !== nextDocumentGroupId), - isLastInList: ( - messageIndex === senderGroup.length - 1 - && senderGroupIndex === senderGroupsArray.length - 1 - && dateGroupIndex === dateGroupsArray.length - 1 - ), - }; - - currentDocumentGroupId = documentGroupId; - - const originalId = getMessageOriginalId(message); - // Scheduled messages can have local IDs in the middle of the list, - // and keys should be ordered, so we prefix it with a date. - // However, this may lead to issues if server date is not synchronized with the local one. - const key = type !== 'scheduled' ? originalId : `${message.date}_${originalId}`; - - return compact([ - message.id === memoFirstUnreadIdRef.current ? unreadDivider : undefined, - , - message.id === threadTopMessageId && ( -
- {lang('DiscussionStarted')} -
- ), - ]); - })); - }); - - return ( -
-
openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined} - > - - {isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && ( - lang('MessageScheduledUntilOnline') - )} - {isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && ( - lang('MessageScheduledOn', formatHumanDate(lang, dateGroup.datetime, undefined, true)) - )} - {!isSchedule && formatHumanDate(lang, dateGroup.datetime)} - -
- {flatten(senderGroups)} -
- ); - }); - - return flatten(dateGroups); -} - export default memo(withGlobal( (global, { chatId, threadId, type }): StateProps => { const chat = selectChat(global, chatId); @@ -797,8 +545,6 @@ export default memo(withGlobal( }, (setGlobal, actions): DispatchProps => pick(actions, [ 'loadViewportMessages', - 'markMessageListRead', - 'markMessagesRead', 'setScrollOffset', 'openHistoryCalendar', ]), diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx new file mode 100644 index 000000000..90484350e --- /dev/null +++ b/src/components/middle/MessageListContent.tsx @@ -0,0 +1,241 @@ +import { RefObject } from 'react'; +import React, { FC, memo } from '../../lib/teact/teact'; + +import { MessageListType } from '../../global/types'; + +import { SCHEDULED_WHEN_ONLINE } from '../../config'; +import buildClassName from '../../util/buildClassName'; +import { compact, flatten } from '../../util/iteratees'; +import { formatHumanDate } from '../../util/dateFormat'; +import { getMessageOriginalId, isActionMessage, isOwnMessage } from '../../modules/helpers'; +import useLang from '../../hooks/useLang'; +import { isAlbum, MessageDateGroup } from './helpers/groupMessages'; +import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; +import useScrollHooks from './hooks/useScrollHooks'; +import useMessageObservers from './hooks/useMessageObservers'; + +import Message from './message/Message'; +import ActionMessage from './ActionMessage'; + +interface OwnProps { + messageIds: number[]; + messageGroups: MessageDateGroup[]; + isViewportNewest: boolean; + isUnread: boolean; + withUsers: boolean; + noAvatars: boolean; + containerRef: RefObject; + anchorIdRef: { current: string | undefined }; + memoFirstUnreadIdRef: { current: number | undefined }; + threadId: number; + type: MessageListType; + threadTopMessageId: number | undefined; + hasLinkedChat: boolean | undefined; + isSchedule: boolean; + noAppearanceAnimation: boolean; + onFabToggle: AnyToVoidFunction; + onNotchToggle: AnyToVoidFunction; + openHistoryCalendar: Function; +} + +const UNREAD_DIVIDER_CLASS = 'unread-divider'; + +const MessageListContent: FC = ({ + messageIds, + messageGroups, + isViewportNewest, + isUnread, + withUsers, + noAvatars, + containerRef, + anchorIdRef, + memoFirstUnreadIdRef, + threadId, + type, + threadTopMessageId, + hasLinkedChat, + isSchedule, + noAppearanceAnimation, + onFabToggle, + onNotchToggle, + openHistoryCalendar, +}) => { + const { + observeIntersectionForMedia, + observeIntersectionForReading, + observeIntersectionForAnimatedStickers, + } = useMessageObservers(type, containerRef, memoFirstUnreadIdRef); + + const { + backwardsTriggerRef, + forwardsTriggerRef, + fabTriggerRef, + } = useScrollHooks( + type, + containerRef, + messageIds, + isViewportNewest, + isUnread, + onFabToggle, + onNotchToggle, + ); + + const lang = useLang(); + + const unreadDivider = ( +
+ {lang('UnreadMessages')} +
+ ); + + const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => { + return acc + flatten(messageGroup.senderGroups).length; + }, 0); + let appearanceIndex = 0; + + const dateGroups = messageGroups.map(( + dateGroup: MessageDateGroup, + dateGroupIndex: number, + dateGroupsArray: MessageDateGroup[], + ) => { + const senderGroups = dateGroup.senderGroups.map(( + senderGroup, + senderGroupIndex, + senderGroupsArray, + ) => { + if (senderGroup.length === 1 && !isAlbum(senderGroup[0]) && isActionMessage(senderGroup[0])) { + const message = senderGroup[0]; + const isLastInList = ( + senderGroupIndex === senderGroupsArray.length - 1 + && dateGroupIndex === dateGroupsArray.length - 1 + ); + + return compact([ + message.id === memoFirstUnreadIdRef.current && unreadDivider, + , + ]); + } + + let currentDocumentGroupId: string | undefined; + + return flatten(senderGroup.map(( + messageOrAlbum, + messageIndex, + ) => { + const message = isAlbum(messageOrAlbum) ? messageOrAlbum.mainMessage : messageOrAlbum; + const album = isAlbum(messageOrAlbum) ? messageOrAlbum : undefined; + const isOwn = isOwnMessage(message); + const isMessageAlbum = isAlbum(messageOrAlbum); + const nextMessage = senderGroup[messageIndex + 1]; + + if (message.previousLocalId && anchorIdRef.current === `message${message.previousLocalId}`) { + anchorIdRef.current = `message${message.id}`; + } + + const documentGroupId = !isMessageAlbum && message.groupedId ? message.groupedId : undefined; + const nextDocumentGroupId = nextMessage && !isAlbum(nextMessage) ? nextMessage.groupedId : undefined; + + const position = { + isFirstInGroup: messageIndex === 0, + isLastInGroup: messageIndex === senderGroup.length - 1, + isFirstInDocumentGroup: Boolean(documentGroupId && documentGroupId !== currentDocumentGroupId), + isLastInDocumentGroup: Boolean(documentGroupId && documentGroupId !== nextDocumentGroupId), + isLastInList: ( + messageIndex === senderGroup.length - 1 + && senderGroupIndex === senderGroupsArray.length - 1 + && dateGroupIndex === dateGroupsArray.length - 1 + ), + }; + + currentDocumentGroupId = documentGroupId; + + const originalId = getMessageOriginalId(message); + // Scheduled messages can have local IDs in the middle of the list, + // and keys should be ordered, so we prefix it with a date. + // However, this may lead to issues if server date is not synchronized with the local one. + const key = type !== 'scheduled' ? originalId : `${message.date}_${originalId}`; + + return compact([ + message.id === memoFirstUnreadIdRef.current ? unreadDivider : undefined, + , + message.id === threadTopMessageId && ( +
+ {lang('DiscussionStarted')} +
+ ), + ]); + })); + }); + + return ( +
+
openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined} + > + + {isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && ( + lang('MessageScheduledUntilOnline') + )} + {isSchedule && dateGroup.originalDate !== SCHEDULED_WHEN_ONLINE && ( + lang('MessageScheduledOn', formatHumanDate(lang, dateGroup.datetime, undefined, true)) + )} + {!isSchedule && formatHumanDate(lang, dateGroup.datetime)} + +
+ {flatten(senderGroups)} +
+ ); + }); + + return ( +
+
+ {flatten(dateGroups)} +
+
+
+ ); +}; + +export default memo(MessageListContent); diff --git a/src/components/middle/hooks/useMessageObservers.ts b/src/components/middle/hooks/useMessageObservers.ts new file mode 100644 index 000000000..d68d174f9 --- /dev/null +++ b/src/components/middle/hooks/useMessageObservers.ts @@ -0,0 +1,80 @@ +import { RefObject } from 'react'; +import { getDispatch } from '../../../lib/teact/teactn'; + +import { MessageListType } from '../../../global/types'; + +import { IS_ANDROID, IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; +import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; +import useBackgroundMode from '../../../hooks/useBackgroundMode'; + +const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350; +const INTERSECTION_MARGIN_FOR_MEDIA = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500; + +export default function useMessageObservers( + type: MessageListType, + containerRef: RefObject, + memoFirstUnreadIdRef: { current: number | undefined }, +) { + const { markMessageListRead, markMessagesRead } = getDispatch(); + + const { + observe: observeIntersectionForMedia, + } = useIntersectionObserver({ + rootRef: containerRef, + throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA, + margin: INTERSECTION_MARGIN_FOR_MEDIA, + }); + + const { + observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading, + } = useIntersectionObserver({ + rootRef: containerRef, + }, (entries) => { + if (type !== 'thread') { + return; + } + + let maxId = 0; + const mentionIds: number[] = []; + + entries.forEach((entry) => { + const { isIntersecting, target } = entry; + + if (!isIntersecting) { + return; + } + + const { dataset } = target as HTMLDivElement; + + const messageId = Number(dataset.lastMessageId || dataset.messageId); + if (messageId > maxId) { + maxId = messageId; + } + + if (dataset.hasUnreadMention) { + mentionIds.push(messageId); + } + }); + + if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) { + markMessageListRead({ maxId }); + } + + if (mentionIds.length) { + markMessagesRead({ messageIds: mentionIds }); + } + }); + + useBackgroundMode(freezeForReading, unfreezeForReading); + + const { observe: observeIntersectionForAnimatedStickers } = useIntersectionObserver({ + rootRef: containerRef, + throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA, + }); + + return { + observeIntersectionForMedia, + observeIntersectionForReading, + observeIntersectionForAnimatedStickers, + }; +} diff --git a/src/components/middle/MessageScroll.tsx b/src/components/middle/hooks/useScrollHooks.ts similarity index 61% rename from src/components/middle/MessageScroll.tsx rename to src/components/middle/hooks/useScrollHooks.ts index 8c248ad01..773efcb89 100644 --- a/src/components/middle/MessageScroll.tsx +++ b/src/components/middle/hooks/useScrollHooks.ts @@ -1,39 +1,39 @@ -import { MutableRefObject } from 'react'; -import React, { FC, useCallback, useRef } from '../../lib/teact/teact'; +import { RefObject } from 'react'; +import { getDispatch } from '../../../lib/teact/teactn'; +import { useCallback, useMemo, useRef } from '../../../lib/teact/teact'; -import { MESSAGE_LIST_SENSITIVE_AREA } from '../../config'; -import resetScroll from '../../util/resetScroll'; -import { useIntersectionObserver, useOnIntersect } from '../../hooks/useIntersectionObserver'; -import useOnChange from '../../hooks/useOnChange'; +import { LoadMoreDirection } from '../../../types'; +import { MessageListType } from '../../../global/types'; -type OwnProps = { - containerRef: MutableRefObject; - className: string; - messageIds: number[]; - loadMoreForwards?: NoneToVoidFunction; - loadMoreBackwards?: NoneToVoidFunction; - isViewportNewest?: boolean; - firstUnreadId?: number; - onFabToggle: AnyToVoidFunction; - onNotchToggle: AnyToVoidFunction; - children: any; -}; +import { debounce } from '../../../util/schedulers'; +import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver'; +import { MESSAGE_LIST_SENSITIVE_AREA } from '../../../config'; +import resetScroll from '../../../util/resetScroll'; +import useOnChange from '../../../hooks/useOnChange'; const FAB_THRESHOLD = 50; const TOOLS_FREEZE_TIMEOUT = 100; -const MessageScroll: FC = ({ - containerRef, - className, - messageIds, - loadMoreForwards, - loadMoreBackwards, - isViewportNewest, - firstUnreadId, - onFabToggle, - onNotchToggle, - children, -}) => { +export default function useScrollHooks( + type: MessageListType, + containerRef: RefObject, + messageIds: number[], + isViewportNewest: boolean, + isUnread: boolean, + onFabToggle: AnyToVoidFunction, + onNotchToggle: AnyToVoidFunction, +) { + const { loadViewportMessages } = getDispatch(); + + const [loadMoreBackwards, loadMoreForwards] = useMemo( + () => (type === 'thread' ? [ + debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Backwards }), 1000, true, false), + debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Forwards }), 1000, true, false), + ] : []), + // eslint-disable-next-line react-hooks/exhaustive-deps + [loadViewportMessages, messageIds], + ); + // eslint-disable-next-line no-null/no-null const backwardsTriggerRef = useRef(null); // eslint-disable-next-line no-null/no-null @@ -59,9 +59,9 @@ const MessageScroll: FC = ({ const isNearBottom = scrollBottom <= FAB_THRESHOLD; const isAtBottom = scrollBottom <= 0; - onFabToggle(firstUnreadId ? !isAtBottom : !isNearBottom); + onFabToggle(isUnread ? !isAtBottom : !isNearBottom); onNotchToggle(!isAtBottom); - }, [messageIds, isViewportNewest, containerRef, onFabToggle, firstUnreadId, onNotchToggle]); + }, [messageIds, isViewportNewest, containerRef, onFabToggle, isUnread, onNotchToggle]); const { observe: observeIntersection, @@ -124,22 +124,5 @@ const MessageScroll: FC = ({ }, TOOLS_FREEZE_TIMEOUT); }, [messageIds]); - return ( -
-
- {children} -
-
-
- ); -}; - -export default MessageScroll; + return { backwardsTriggerRef, forwardsTriggerRef, fabTriggerRef }; +}