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

LG-4789: Chat MessageFeed Scroll to Latest Button #2648

Merged
merged 14 commits into from
Jan 28, 2025
5 changes: 5 additions & 0 deletions .changeset/fifty-bears-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lg-chat/message-feed": minor
---

Improves autoscroll behavior. If a user scrolls up, autoscroll turns off and instead we show a "scroll to latest" button.
22 changes: 15 additions & 7 deletions chat/message-feed/src/MessageFeed.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ChangeEvent, useState } from 'react';
import React, { ChangeEvent, Fragment, useState } from 'react';
import { Avatar } from '@lg-chat/avatar';
import { DisclaimerText } from '@lg-chat/chat-disclaimer';
import { LeafyGreenChatProvider } from '@lg-chat/leafygreen-chat-provider';
Expand Down Expand Up @@ -115,10 +115,14 @@ export const ChangingMessages: StoryType<typeof MessageFeed> = ({
const toggleShouldAddMongoMessage = () =>
setShouldAddMongoMessage(should => !should);

const handleButtonClick = () => {
const shortMessage = 'This is a new message';
const longMessage =
"To perform semantic search on your data using MongoDB Atlas, follow these best practices:\n\n1. **Create a Search Index**:\n - Define a search index for your collection. This index will categorize your data in a searchable format, enabling faster retrieval of documents.\n\n2. **Use Analyzers for Tokenization**:\n - Pre-process your data with analyzers to transform it into a sequence of tokens. This includes tokenization, normalization, and stemming. Choose the appropriate analyzer based on your data and application needs.\n\n3. **Construct Search Queries**:\n - Build and run search queries using the `$search` aggregation pipeline stage. These queries can be simple text matches or more complex queries involving phrases, number or date ranges, regular expressions, or wildcards.\n\n4. **Customize Relevance Scores**:\n - Modify the ranking of search results to boost certain documents or match specific relevance requirements. This helps in tailoring the search results to your application's domain.\n\n5. **Use Compound Queries**:\n - Combine multiple operators and clauses in a single search query using the compound operator. This allows for more complex and refined search queries.\n\n6. **Implement Synonyms**:\n - Configure synonyms to index and search collections for words with similar meanings. This enhances the search experience by accounting for different terms users might use.\n\n7. **Filter and Parse Results**:\n - Use facets to group and count search results by multiple categories. This helps in quickly filtering and parsing the search results.\n\nBy following these best practices, you can optimize your semantic search capabilities in MongoDB Atlas and provide a more relevant and efficient search experience for your users.";

const handleButtonClick = (messageBody = shortMessage) => {
const newMessage: MessageFields = {
id: newMessageId.toString(),
messageBody: 'This is a new message',
messageBody,
...(shouldAddMongoMessage
? {
isMongo: true,
Expand Down Expand Up @@ -147,9 +151,8 @@ export const ChangingMessages: StoryType<typeof MessageFeed> = ({
const { id, isMongo, messageBody, userName } =
message as MessageFields;
return (
<>
<Fragment key={id}>
<Message
key={id}
sourceType="markdown"
darkMode={darkMode}
avatar={
Expand All @@ -172,12 +175,17 @@ export const ChangingMessages: StoryType<typeof MessageFeed> = ({
</MessagePrompt>
</MessagePrompts>
)}
</>
</Fragment>
);
})}
</MessageFeed>
</LeafyGreenChatProvider>
<button onClick={handleButtonClick}>Click me to add a message</button>
<button onClick={() => handleButtonClick(shortMessage)}>
Click me to add a message
</button>
<button onClick={() => handleButtonClick(longMessage)}>
Click me to add a long message
</button>
</div>
);
};
15 changes: 14 additions & 1 deletion chat/message-feed/src/MessageFeed/MessageFeed.styles.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { avatarSizes, Size } from '@lg-chat/avatar';

Check failure on line 1 in chat/message-feed/src/MessageFeed/MessageFeed.styles.ts

View workflow job for this annotation

GitHub Actions / Check lints

Run autofix to sort these imports!

import { css } from '@leafygreen-ui/emotion';
import { Theme } from '@leafygreen-ui/lib';
import { palette } from '@leafygreen-ui/palette';
import { breakpoints, spacing } from '@leafygreen-ui/tokens';
import { breakpoints, borderRadius, spacing } from '@leafygreen-ui/tokens';

export const baseStyles = css`
height: 500px;
Expand Down Expand Up @@ -104,3 +104,16 @@
margin-top: ${spacing[4]}px;
margin-bottom: ${spacing[6]}px;
`;

export const scrollButtonStyles = css`
position: sticky;
bottom: 0;
width: 100%;
display: flex;
justify-content: center;

> button {
box-shadow: 0 ${spacing[50]}px ${spacing[100]}px rgba(0, 0, 0, 0.2);
border-radius: ${borderRadius[400]}px;
}
`;
91 changes: 88 additions & 3 deletions chat/message-feed/src/MessageFeed/MessageFeed.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { ForwardedRef, forwardRef, useEffect, useRef } from 'react';
import React, {

Check failure on line 1 in chat/message-feed/src/MessageFeed/MessageFeed.tsx

View workflow job for this annotation

GitHub Actions / Check lints

Run autofix to sort these imports!
ForwardedRef,
forwardRef,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import flattenChildren from 'react-keyed-flatten-children';
import { useLeafyGreenChatContext } from '@lg-chat/leafygreen-chat-provider';

Expand All @@ -8,6 +15,8 @@
} from '@leafygreen-ui/leafygreen-provider';
import { isComponentType } from '@leafygreen-ui/lib';
import { breakpoints } from '@leafygreen-ui/tokens';
import Icon from '@leafygreen-ui/icon';
import Button, { Size as ButtonSize } from '@leafygreen-ui/button';
nlarew marked this conversation as resolved.
Show resolved Hide resolved

import {
avatarPaddingStyles,
Expand All @@ -16,6 +25,7 @@
disclaimerTextStyles,
messageFeedStyles,
messageFeedThemeStyles,
scrollButtonStyles,
themeStyles,
} from './MessageFeed.styles';
import { MessageFeedProps } from '.';
Expand Down Expand Up @@ -55,11 +65,56 @@
}
});

useEffect(() => {
const [showScrollButton, setShowScrollButton] = useState(false);
const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const scrollToLatest = useCallback(() => {
if (containerRef.current) {
containerRef.current.scrollTo(0, containerRef.current.scrollHeight);
}
}, [children]);
}, []);

useEffect(() => {
const scrollElement = containerRef.current;
if (!scrollElement) return;

const isScrolledToEnd = () => {
if (!containerRef.current) return true;
const { scrollHeight, scrollTop, clientHeight } = containerRef.current;
// Add a small buffer (2px) to account for floating point differences
return scrollHeight - scrollTop - clientHeight <= 2;
};

// Handle scroll events
const handleScroll = () => {
// Clear any existing timeout
if (scrollTimerRef.current) {
clearTimeout(scrollTimerRef.current);
}

// Wait until scroll animation completes This avoids a brief flicker
// when the user scrolls to the bottom
const scrollDuration = 100;
scrollTimerRef.current = setTimeout(() => {
setShowScrollButton(!isScrolledToEnd());
}, scrollDuration);
};

scrollElement.addEventListener('scroll', handleScroll);

return () => {
scrollElement.removeEventListener('scroll', handleScroll);
if (scrollTimerRef.current) {
clearTimeout(scrollTimerRef.current);
}
};
}, []);

useEffect(() => {
if (!showScrollButton) {
scrollToLatest();
}
}, [children, showScrollButton]);

Check warning on line 117 in chat/message-feed/src/MessageFeed/MessageFeed.tsx

View workflow job for this annotation

GitHub Actions / Check lints

React Hook useEffect has a missing dependency: 'scrollToLatest'. Either include it or remove the dependency array

return (
<LeafyGreenProvider darkMode={darkMode}>
Expand All @@ -73,6 +128,11 @@
ref={containerRef}
>
{renderedChildren}
<ScrollToLatestButton
visible={showScrollButton}
handleScroll={scrollToLatest}
darkMode={darkMode}
/>
</div>
</div>
</LeafyGreenProvider>
Expand All @@ -81,3 +141,28 @@
);

MessageFeed.displayName = 'MessageFeed';

function ScrollToLatestButton({
visible,
handleScroll,
darkMode: darkModeProp,
}: {
visible: boolean;
handleScroll: () => void;
darkMode?: boolean;
}) {
const { darkMode } = useDarkMode(darkModeProp);
return visible ? (
<div className={scrollButtonStyles}>
<Button
onClick={handleScroll}
darkMode={darkMode}
aria-label="Scroll to latest message"
size={ButtonSize.Small}
rightGlyph={<Icon glyph="ArrowDown" />}
>
Scroll to latest
</Button>
</div>
) : null;
}
Loading