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

(EAI-71) [UI] Rich Links in LG Chat #2328

Merged
merged 25 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/angry-hornets-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lg-chat/message': minor
shaneeza marked this conversation as resolved.
Show resolved Hide resolved
---

Adds the links prop which renders link data as rich links after the message content
5 changes: 5 additions & 0 deletions .changeset/wicked-ladybugs-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lg-chat/rich-links': major
---

Initial release
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ export const WithMessageRating: StoryFn<typeof MessageRating> = args => {
{...args}
value={rating}
onChange={handleRatingChange}
className={css`
margin-bottom: ${spacing[1]}px;
`}
hideThumbsUp={isSubmitted}
/>
{rating === 'disliked' && (!isSubmitted || isDisplayingSubmitted) && (
Expand Down
45 changes: 32 additions & 13 deletions chat/message/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,35 @@ return (

## Properties

| Prop | Type | Description | Default |
| -------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `align` | `'left', 'right'` | Determines whether the message is aligned to the left or right | if `isSender === true`, the message is aligned to the right, and otherwise to the left. This prop overrides that behavior |
| `avatar` | `ReactElement` | Avatar element | |
| `componentOverrides` | `Record<MarkdownComponent, ComponentType>` | Uses value to override key'ed markdown elements in terms of how they are rendered | |
| `children` | `string` | Rendered children; only string children are supported | |
| `isSender` | `boolean` | Indicates if the message is from the current user | `true` |
| `markdownProps` | `LGMarkdownProps` | Props passed to the internal ReactMarkdown instance | |
| `messageRatingProps` | `MessageRatingProps` | Props to MessageRating component | |
| `sourceType` | `'markdown', 'text'` | Determines the rendering method of the message | |
| `messageBody` | `string` | Message body text passed to LGMarkdown | |
| `verified` | `{ verifier?: string; verifiedAt?: Date; learnMoreUrl?: string; }` | Sets if an answer is "verified" and controls the content of the message banner. | |
| `...` | `HTMLElementProps<'div'>` | Props spread on the root element | |
| Prop | Type | Description | Default |
| -------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `align` | `'left', 'right'` | Determines whether the message is aligned to the left or right | if `isSender === true`, the message is aligned to the right, and otherwise to the left. This prop overrides that behavior |
| `avatar` | `ReactElement` | Avatar element | |
| `componentOverrides` | `Record<MarkdownComponent, ComponentType>` | Uses value to override key'ed markdown elements in terms of how they are rendered | |
| `children` | `string` | Rendered children; only string children are supported | |
| `isSender` | `boolean` | Indicates if the message is from the current user | `true` |
| `markdownProps` | `LGMarkdownProps` | Props passed to the internal ReactMarkdown instance | |
| `messageRatingProps` | `MessageRatingProps` | Props to MessageRating component | |
| `sourceType` | `'markdown', 'text'` | Determines the rendering method of the message | |
| `messageBody` | `string` | Message body text passed to LGMarkdown | |
| `verified` | `{ verifier?: string; verifiedAt?: Date; learnMoreUrl?: string; }` | Sets if an answer is "verified" and controls the content of the message banner. | |
| `links` | `{ url: string; text: string; imageUrl?: string; variant: string; }[]` | A list of links to show in a section at the end of the message. | |
shaneeza marked this conversation as resolved.
Show resolved Hide resolved
| `linksHeading` | `string` | The heading text to display for the links section. | "Related Resources" |
| `...` | `HTMLElementProps<'div'>` | Props spread on the root element | |

### Message Links

The message component includes the following built-in `variant` values for the `links` prop:

- `"Blog"`
- `"Book"`
- `"Code"`
- `"Docs"`
- `"Learn"`
- `"Video"`
- `"Website"`

These map to pre-defined badge glyphs, labels, and colors for specific use
cases. If no variant serves your use case, you can create a custom link by
omitting the `variant` prop and defining the `badgeGlyph`, `badgeLabel`, and
optionally `badgeVariant` props.
3 changes: 2 additions & 1 deletion chat/message/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"@leafygreen-ui/polymorphic": "^1.3.7",
"@leafygreen-ui/tokens": "^2.5.2",
"@leafygreen-ui/typography": "^19.0.0",
"@lg-chat/lg-markdown": "^2.0.2"
"@lg-chat/lg-markdown": "^2.0.2",
"@lg-chat/rich-links": "^0.1.0"
},
"devDependencies": {
"@lg-chat/avatar": "^3.0.1",
Expand Down
153 changes: 100 additions & 53 deletions chat/message/src/Message/Message.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { WithMessageRating as MessageFeedbackStory } from '@lg-chat/message-feed
import { storybookArgTypes, StoryMetaType } from '@lg-tools/storybook-utils';
import { StoryFn } from '@storybook/react';

import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography';

import { Message, MessageSourceType } from '..';

const MarkdownText = `
Expand Down Expand Up @@ -42,25 +44,24 @@ function helloWorld() {
const UserText = `How can I delete a massive amount of documents from a collection?`;

const MongoText = `
To efficiently delete a large number of documents from a MongoDB collection, you
can use the \`deleteMany()\` method. This method allows you to delete multiple
documents that match a specified filter.
To efficiently delete a large number of documents from a MongoDB collection, you can use the \`deleteMany()\` method. This method allows you to delete multiple documents that match a specified filter.

Here's an example of how to use the \`deleteMany()\` method to delete a large
number of documents:
Here's an example of how to use the \`deleteMany()\` method to delete a large number of documents:

\`\`\`javascript
db.collection.deleteMany({ <filter> });
\`\`\`

Keep in mind that deleting a large number of documents can be resource-intensive
and may impact the performance of your MongoDB server. It's recommended to
perform such operations during periods of low activity or to use techniques like
sharding to distribute the load across multiple servers.
Keep in mind that deleting a large number of documents can be resource-intensive and may impact the performance of your MongoDB server. It's recommended to perform such operations during periods of low activity or to use techniques like sharding to distribute the load across multiple servers.

Let me know if you need any further assistance!
`;

const MessageFeedback = () => {
// @ts-ignore onChange is passed in the story itself
return <MessageFeedbackStory />;
};

const meta: StoryMetaType<typeof Message> = {
title: 'Chat/Message',
component: Message,
Expand All @@ -82,10 +83,8 @@ const meta: StoryMetaType<typeof Message> = {
};
export default meta;

// eslint-disable-next-line react/prop-types
const Template: StoryFn<typeof Message> = ({ darkMode, avatar, ...rest }) => {
const Avatar = avatar ? React.cloneElement(avatar, { darkMode }) : undefined;
return <Message avatar={Avatar} darkMode={darkMode} {...rest} />;
const Template: StoryFn<typeof Message> = props => {
return <Message {...props}></Message>;
};

export const Basic: StoryFn<typeof Message> = Template.bind({});
Expand Down Expand Up @@ -117,8 +116,7 @@ WithMessageRating.args = {
isSender: false,
messageBody: MongoText,
avatar: <Avatar variant="mongo" />,
// @ts-ignore onChange is passed in the story itself
children: <MessageFeedbackStory />,
children: <MessageFeedback />,
};

export const VerifiedAnswer: StoryFn<typeof Message> = Template.bind({});
Expand All @@ -130,43 +128,92 @@ VerifiedAnswer.args = {
verifier: 'MongoDB Staff',
verifiedAt: new Date('2023-08-24T16:20:00Z'),
},
// @ts-ignore onChange is passed in the story itself
children: <MessageFeedbackStory />,
children: <MessageFeedback />,
};

export const MultipleUser = () => {
const baseFontSize = useUpdatedBaseFontSize();
return (
<LeafyGreenChatProvider>
<div>
<Basic {...meta.args} {...Basic.args} baseFontSize={baseFontSize} />
<Basic
{...meta.args}
{...Basic.args}
messageBody="Another message!"
baseFontSize={baseFontSize}
/>
</div>
</LeafyGreenChatProvider>
);
};

export const MultipleUser = () => (
<LeafyGreenChatProvider>
<div>
{/* @ts-expect-error baseFontSize is not a number */}
<Basic {...meta.args} {...Basic.args} />
{/* @ts-expect-error baseFontSize is not a number */}
<Basic {...meta.args} {...Basic.args} messageBody="Another message!" />
</div>
</LeafyGreenChatProvider>
);

export const MultipleMongo = () => (
<LeafyGreenChatProvider>
<div>
{/* @ts-expect-error baseFontSize is not a number */}
<Mongo
{...meta.args}
{...Mongo.args}
messageBody="First message! Expect another from me right after this one."
/>
{/* @ts-expect-error baseFontSize is not a number */}
<Mongo {...meta.args} {...Mongo.args} />
</div>
</LeafyGreenChatProvider>
);

export const Alternating = () => (
<LeafyGreenChatProvider>
<div>
{/* @ts-expect-error baseFontSize is not a number */}
<Basic {...meta.args} {...Basic.args} />
{/* @ts-expect-error baseFontSize is not a number */}
<Mongo {...meta.args} {...Mongo.args} />
</div>
</LeafyGreenChatProvider>
);
export const MultipleMongo = () => {
const baseFontSize = useUpdatedBaseFontSize();
return (
<LeafyGreenChatProvider>
<div>
<Mongo
{...meta.args}
{...Mongo.args}
messageBody="First message! Expect another from me right after this one."
baseFontSize={baseFontSize}
/>
<Mongo {...meta.args} {...Mongo.args} baseFontSize={baseFontSize} />
</div>
</LeafyGreenChatProvider>
);
};

export const Alternating = () => {
const baseFontSize = useUpdatedBaseFontSize();
return (
<LeafyGreenChatProvider>
<div>
<Basic {...meta.args} {...Basic.args} baseFontSize={baseFontSize} />
<Mongo {...meta.args} {...Mongo.args} baseFontSize={baseFontSize} />
</div>
</LeafyGreenChatProvider>
);
};

export const WithRichLinks: StoryFn<typeof Message> = Template.bind({});
WithRichLinks.args = {
isSender: false,
messageBody: MongoText,
avatar: <Avatar variant="mongo" />,
children: <MessageFeedback />,
links: [
{
href: 'https://mongodb.design',
children: 'LeafyGreen UI',
variant: 'Website',
},
{
href: 'https://mongodb.github.io/leafygreen-ui/?path=/docs/overview-introduction--docs',
children: 'LeafyGreen UI Docs',
variant: 'Docs',
},
{
href: 'https://learn.mongodb.com/',
children: 'MongoDB University',
variant: 'Learn',
},
{
href: 'https://mongodb.com/docs',
children: 'MongoDB Docs',
variant: 'Docs',
},
{
href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
children: 'Rick Astley - Never Gonna Give You Up',
variant: 'Video',
imageUrl: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
},
{
href: 'https://mongodb.com/',
children: 'MongoDB Homepage',
variant: 'Website',
},
],
};
2 changes: 1 addition & 1 deletion chat/message/src/Message/Message.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const avatarClassName = createUniqueClassName('lg-message-avatar');

export const baseStyles = css`
display: flex;
gap: ${spacing[2]}px;
gap: ${spacing[200]}px;
nlarew marked this conversation as resolved.
Show resolved Hide resolved
align-items: flex-end;
width: 100%;

Expand Down
10 changes: 10 additions & 0 deletions chat/message/src/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { BaseFontSize, breakpoints } from '@leafygreen-ui/tokens';
import { VerifiedAnswerBanner } from '../MessageBanner';
import { MessageContainer, Variant } from '../MessageContainer';
import { MessageContent } from '../MessageContent';
import { MessageLinks } from '../MessageLinks';

import {
avatarClassName,
Expand Down Expand Up @@ -46,6 +47,8 @@ export const Message = forwardRef(
className,
children,
componentOverrides,
links,
linksHeading,
markdownProps,
verified,
darkMode: darkModeProp,
Expand Down Expand Up @@ -136,6 +139,13 @@ export const Message = forwardRef(
>
{messageBody ?? ''}
</Polymorph>
{links ? (
<Polymorph
as={componentOverrides?.MessageLinks ?? MessageLinks}
headingText={linksHeading}
links={links}
/>
) : null}
{children}
</Polymorph>
</div>
Expand Down
19 changes: 16 additions & 3 deletions chat/message/src/Message/Message.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ReactElement } from 'react';
import { type RichLinkProps } from '@lg-chat/rich-links';

import { DarkModeProps, HTMLElementProps } from '@leafygreen-ui/lib';
import { type DarkModeProps, type HTMLElementProps } from '@leafygreen-ui/lib';
import { BaseFontSize } from '@leafygreen-ui/tokens';

import { MessageContainerProps } from '../MessageContainer';
import { MessageContentProps } from '../MessageContent';
import { type MessageContainerProps } from '../MessageContainer';
import { type MessageContentProps } from '../MessageContent';
import { type MessageLinksProps } from '../MessageLinks';

export const Align = {
Right: 'right',
Expand All @@ -16,6 +18,7 @@ export type Align = (typeof Align)[keyof typeof Align];
export interface ComponentOverrides {
MessageContainer?: (props: MessageContainerProps) => JSX.Element;
MessageContent?: (props: MessageContentProps) => JSX.Element;
MessageLinks?: (props: MessageLinksProps) => JSX.Element;
}

export interface MessageProps
Expand Down Expand Up @@ -57,6 +60,16 @@ export interface MessageProps
* displays information about the message.
*/
verified?: VerificationInfo;

/**
* A list of links to render as rich links for the message.
*/
links?: Array<RichLinkProps>;

/**
* The heading text to display for the links section.
*/
linksHeading?: string;
}

export interface VerificationInfo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const baseStyles = css`
box-shadow: 0px 4px 10px -4px ${palette.black}4D; // 4D is 30% opacity

position: relative;

display: flex;
flex-direction: column;
gap: ${spacing[200]}px;
`;

export const variantStyles: Record<Variant, Record<Theme, string>> = {
Expand Down
22 changes: 22 additions & 0 deletions chat/message/src/MessageLinks/MessageLinks.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { css } from '@leafygreen-ui/emotion';
import { Theme } from '@leafygreen-ui/lib';
import { palette } from '@leafygreen-ui/palette';
import { spacing } from '@leafygreen-ui/tokens';

export const baseStyles = css`
container-type: inline-size;
shaneeza marked this conversation as resolved.
Show resolved Hide resolved
margin-bottom: ${spacing[200]}px;
`;

export const dividingLineStyles: Record<Theme, string> = {
[Theme.Dark]: css`
border: 1px solid ${palette.gray.dark2};
`,
[Theme.Light]: css`
border: 1px solid ${palette.gray.light2};
`,
};

export const linksHeadingStyles = css`
margin-bottom: ${spacing[200]}px;
`;
Loading
Loading