Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Scroll to bottom in a click. #33284

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
6 changes: 6 additions & 0 deletions .changeset/strong-fans-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/i18n': minor
'@rocket.chat/meteor': minor
---

Improves user experience by adding a jump to bottom button which gets you to bottom of chat room in a click making scrolling easy and more efficient
54 changes: 54 additions & 0 deletions apps/meteor/client/views/room/body/JumpToBottomButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Bubble } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import React from 'react';

import { isTruthy } from '../../../../lib/isTruthy';

type JumpToBottomButtonProps = {
visible: boolean;
onClick: () => void;
text: string;
};

const buttonStyle = css`
position: absolute;
z-index: 2;
bottom: 8px;
left: 50%;
user-select: none;
transform: translate(-50%, 0);

&.not {
visibility: hidden;
}

@keyframes fadeout {
50% {
visibility: visible;
transform: translate(-50%, 150%);
}
100% {
visibility: hidden;
transform: translate(-50%, 150%);
position: fixed;
}
}
`;

const JumpToBottomButton = ({ visible, onClick, text }: JumpToBottomButtonProps): ReactElement => {
return (
<Box className={[buttonStyle, !visible && 'not'].filter(isTruthy)}>
<Bubble
icon='arrow-down'
onClick={() => {
onClick();
}}
>
{text}
</Bubble>
</Box>
);
};

export default JumpToBottomButton;
21 changes: 18 additions & 3 deletions apps/meteor/client/views/room/body/RoomBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { useMessageListNavigation } from '../hooks/useMessageListNavigation';
import { useRetentionPolicy } from '../hooks/useRetentionPolicy';
import DropTargetOverlay from './DropTargetOverlay';
import JumpToRecentMessageButton from './JumpToRecentMessageButton';
import JumpToBottomButton from './JumpToBottomButton';
import LeaderBar from './LeaderBar';
import LoadingMessagesIndicator from './LoadingMessagesIndicator';
import RetentionPolicyWarning from './RetentionPolicyWarning';
Expand All @@ -41,6 +42,7 @@ import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl';
import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents';
import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition';
import { useHandleUnread } from './hooks/useUnreadMessages';
import { useIsScrolling } from '../hooks/useIsScrolling';

const RoomBody = (): ReactElement => {
const chat = useChat();
Expand Down Expand Up @@ -100,7 +102,16 @@ const RoomBody = (): ReactElement => {

const { innerRef: dateScrollInnerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll();

const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom();
const { innerRef: isScrollingRef, isScrolling } = useIsScrolling(4000);

const {
innerRef: isAtBottomInnerRef,
atBottomRef,
sendToBottom,
sendToBottomIfNecessary,
isAtBottom,
handleJumpToBottom,
} = useListIsAtBottom();

const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef);

Expand Down Expand Up @@ -133,7 +144,7 @@ const RoomBody = (): ReactElement => {
leaderBannerInnerRef,
unreadBarInnerRef,
getMoreInnerRef,

isScrollingRef,
messageListRef,
);

Expand Down Expand Up @@ -212,7 +223,6 @@ const RoomBody = (): ReactElement => {
name: useRealName ? leaderRoomRole.u.name || leaderRoomRole.u.username : leaderRoomRole.u.username,
};
});

return (
<>
{!isLayoutEmbedded && room.announcement && <Announcement announcement={room.announcement} announcementDetails={undefined} />}
Expand Down Expand Up @@ -261,6 +271,11 @@ const RoomBody = (): ReactElement => {
</Box>

<div className={['messages-box', roomLeader && !hideLeaderHeader && 'has-leader'].filter(isTruthy).join(' ')}>
<JumpToBottomButton
visible={!hasNewMessages && !isAtBottom() && isScrolling}
onClick={handleJumpToBottom}
text={t('Jump_to_bottom')}
/>
<JumpToRecentMessageButton visible={hasNewMessages} onClick={handleNewMessageButtonClick} text={t('New_messages')} />
<JumpToRecentMessageButton
visible={hasMoreNextMessages}
Expand Down
27 changes: 16 additions & 11 deletions apps/meteor/client/views/room/body/hooks/useListIsAtBottom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { withThrottling } from '../../../../../lib/utils/highOrderFunctions';

export const useListIsAtBottom = () => {
const atBottomRef = useRef(true);

const innerBoxRef = useRef<HTMLDivElement | null>(null);

const sendToBottom = useCallback(() => {
Expand All @@ -19,6 +18,10 @@ export const useListIsAtBottom = () => {
}
}, [atBottomRef, sendToBottom]);

const handleJumpToBottom = useCallback(() => {
sendToBottom();
}, []);

const isAtBottom = useCallback((threshold = 0) => {
if (!innerBoxRef.current) {
return true;
Expand Down Expand Up @@ -46,24 +49,26 @@ export const useListIsAtBottom = () => {

observer.observe(messageList);

node.addEventListener(
'scroll',
withThrottling({ wait: 100 })(() => {
atBottomRef.current = isAtBottom(100);
}),
{
passive: true,
},
);
const handleScroll = withThrottling({ wait: 100 })(() => {
atBottomRef.current = isAtBottom(100);
});

node.addEventListener('scroll', handleScroll, {
passive: true,
});

return () => {
node.removeEventListener('scroll', handleScroll);
};
},
[isAtBottom],
);

return {
atBottomRef,
innerRef: useMergedRefs(ref, innerBoxRef) as unknown as React.MutableRefObject<HTMLDivElement | null>,
sendToBottom,
sendToBottomIfNecessary,
isAtBottom,
handleJumpToBottom,
};
};
35 changes: 35 additions & 0 deletions apps/meteor/client/views/room/hooks/useIsScrolling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useState, useCallback, useRef } from 'react';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import { withThrottling } from '/lib/utils/highOrderFunctions';

export const useIsScrolling = (inactivityTimeout = 0) => {
const [isScrolling, setIsScrolling] = useSafely(useState<boolean>(false));
const timeoutRef = useRef<NodeJS.Timeout | null>(null);

const handleScroll = withThrottling({ wait: 100 })(() => {
setIsScrolling(true);

if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

timeoutRef.current = setTimeout(() => {
setIsScrolling(false);
}, inactivityTimeout);
});

const innerRef = useCallback(
(node: HTMLElement | null) => {
if (node) {
node.removeEventListener('scroll', handleScroll);
node.addEventListener('scroll', handleScroll, { passive: true });
}
},
[handleScroll],
);

return {
innerRef,
isScrolling,
};
};
Loading