Skip to content

Commit 7391a4c

Browse files
authored
Merge pull request #329 from eccenca/feature/chatComponents-CMEM-6775
Integrate components to display user chat (CMEM-6775)
2 parents e131f26 + 128a002 commit 7391a4c

20 files changed

Lines changed: 814 additions & 8 deletions

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
88

99
### Added
1010

11+
- `<ChatContent />`
12+
- displays single chat contents in a bubble, including options to add status line and avatar
13+
- `<ChatContentCollapsed />`
14+
- can collapse (and expand) `<ChatContent />` automatically for convenience
15+
- `<ChatField />`
16+
- let the user input texts, calls `onSubmit` handler on enter key and submit button
17+
- `<ChatArea />`
18+
- combine a list of chat contents and user input box
19+
- `<TextReducer />`
20+
- reduces HTML to simple text and can display it as one ellipsed line
1121
- `<Tooltip />`
1222
- prove useage of `usePlaceholder` by jest test coverage
1323

@@ -20,7 +30,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
2030
- `<Tooltip />`
2131
- re-check hover state after swapping the placeholder before triggering the event bubbling
2232

23-
## Changed
33+
### Changed
2434

2535
- `<IconButton/>`
2636
- increase the default delay before swapping the tooltip placeholder of the icon, reducing unwanted swaps because of mouseovers that were not intended

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,11 @@
9191
"n3": "^1.25.1",
9292
"re-resizable": "^6.10.3",
9393
"react": "^16.13.1",
94-
"react-dom": "^16.13.1",
94+
"react-dom": "^16.14.0",
9595
"react-flow-renderer": "9.7.4",
9696
"react-flow-renderer-lts": "npm:react-flow-renderer@^10.3.17",
9797
"react-inlinesvg": "^3.0.3",
98+
"react-is": "^16.13.1",
9899
"react-markdown": "^10.1.0",
99100
"react-markdown-deprecated": "npm:react-markdown@^8.0.7",
100101
"react-syntax-highlighter": "^15.6.1",
@@ -138,6 +139,7 @@
138139
"@types/jshint": "^2.12.4",
139140
"@types/lodash": "^4.17.16",
140141
"@types/n3": "^1.24.2",
142+
"@types/react-is": "^19.0.0",
141143
"@types/react-syntax-highlighter": "^15.5.13",
142144
"@typescript-eslint/eslint-plugin": "^8.30.1",
143145
"@typescript-eslint/parser": "^8.30.1",
@@ -174,8 +176,7 @@
174176
},
175177
"peerDependencies": {
176178
"@blueprintjs/core": ">=5",
177-
"react": ">=16",
178-
"react-dom": ">=16"
179+
"react": ">=16"
179180
},
180181
"resolutions": {
181182
"**/@types/react": "^17.0.85",

src/components/Chat/ChatArea.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React from "react";
2+
3+
import { TestableComponent } from "../../components/interfaces";
4+
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
5+
6+
import { FlexibleLayoutContainer, FlexibleLayoutContainerProps, FlexibleLayoutItem } from "./../FlexibleLayout";
7+
import { Spacing, SpacingProps } from "./../Separation/Spacing";
8+
import { ChatFieldProps } from "./ChatField";
9+
10+
export interface ChatAreaProps
11+
extends Omit<FlexibleLayoutContainerProps, "vertical" | "noEqualItemSpace">,
12+
TestableComponent {
13+
/**
14+
* The inut field for the chat.
15+
*/
16+
chatField?: React.ReactElement<ChatFieldProps>;
17+
/**
18+
* Set the position of the chat field.
19+
*/
20+
chatFieldPosition?: "top" | "bottom";
21+
/**
22+
* Sets the maximum width for chat contents and input.
23+
*/
24+
contentWidth?: "small" | "medium" | "large" | "full";
25+
/**
26+
* Put chat content in a list and add spacings automatically.
27+
* Works best if each `ChatArea` child represents one chat content item.
28+
*/
29+
autoSpacingSize?: SpacingProps["size"];
30+
/**
31+
* Scrolls content to the first or last child automatically.
32+
* The correct value depends on the place where you insert the most recent chat item.
33+
*/
34+
autoScrollTo?: "first" | "last";
35+
}
36+
37+
/**
38+
* Component to display a full chat, containing chat content bubbles and text input.
39+
*/
40+
export const ChatArea = ({
41+
children,
42+
className,
43+
chatField,
44+
chatFieldPosition = "bottom",
45+
contentWidth = "medium",
46+
autoSpacingSize,
47+
gapSize = "medium",
48+
autoScrollTo,
49+
...otherFlexibleLayoutContainerProps
50+
}: ChatAreaProps) => {
51+
const chatcontents = React.useRef<HTMLDivElement>(null);
52+
53+
React.useEffect(() => {
54+
if (chatcontents.current && children && autoScrollTo) {
55+
const chatitems = chatcontents.current.getElementsByClassName(`${eccgui}-chat__content`);
56+
if (chatitems.length > 0) {
57+
chatitems[autoScrollTo === "first" ? 0 : chatitems.length - 1].scrollIntoView({
58+
behavior: "instant",
59+
block: autoScrollTo === "first" ? "start" : "end",
60+
});
61+
}
62+
}
63+
}, [chatcontents, children, autoScrollTo]);
64+
65+
return (
66+
<FlexibleLayoutContainer
67+
className={
68+
`${eccgui}-chat__area` + ` ${eccgui}-chat__area--${contentWidth}` + (className ? ` ${className}` : "")
69+
}
70+
vertical
71+
noEqualItemSpace
72+
gapSize={gapSize}
73+
{...otherFlexibleLayoutContainerProps}
74+
>
75+
{chatField && (
76+
<FlexibleLayoutItem
77+
growFactor={0}
78+
shrinkFactor={0}
79+
style={chatFieldPosition === "bottom" ? { order: 1 } : undefined}
80+
>
81+
<div className={`${eccgui}-chat__area-contentwidth`}>{chatField}</div>
82+
</FlexibleLayoutItem>
83+
)}
84+
<FlexibleLayoutItem
85+
style={
86+
otherFlexibleLayoutContainerProps.useAbsoluteSpace
87+
? {
88+
overflow: "auto",
89+
minHeight: 0,
90+
padding: "2px 0",
91+
}
92+
: undefined
93+
}
94+
>
95+
<div className={`${eccgui}-chat__area-contentwidth`} ref={chatcontents}>
96+
{autoSpacingSize && children ? (
97+
<ul>
98+
{React.Children.toArray(children).map((child, index) => (
99+
<li key={index}>
100+
{child}
101+
<Spacing size={autoSpacingSize} />
102+
</li>
103+
))}
104+
</ul>
105+
) : (
106+
children
107+
)}
108+
</div>
109+
</FlexibleLayoutItem>
110+
</FlexibleLayoutContainer>
111+
);
112+
};
113+
114+
export default ChatArea;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React from "react";
2+
3+
import { TestableComponent } from "../../components/interfaces";
4+
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
5+
6+
import { Markdown, MarkdownProps } from "./../../cmem/markdown/Markdown";
7+
import { ContextMenuProps } from "./../ContextOverlay/ContextMenu";
8+
import { DepictionProps } from "./../Depiction/Depiction";
9+
import { FlexibleLayoutContainer, FlexibleLayoutItem } from "./../FlexibleLayout";
10+
import { IconButtonProps } from "./../Icon/IconButton";
11+
import { Spacing } from "./../Separation/Spacing";
12+
import { HtmlContentBlock, OverflowTextProps } from "./../Typography";
13+
14+
export interface ChatContentProps extends React.HTMLAttributes<HTMLDivElement>, TestableComponent {
15+
/**
16+
* Should be a line of text, e.g. username, timestamp, ...
17+
*/
18+
statusLine?: React.ReactElement<OverflowTextProps>;
19+
/**
20+
* How the chat content box is displayed.
21+
*/
22+
displayType?: "free" | "simple" | "bubble";
23+
/**
24+
* A depiction used as avatar next to the content box.
25+
*/
26+
avatar?: React.ReactElement<DepictionProps>;
27+
/**
28+
* If indented then the content box has some white space on the opposite side to the alignment
29+
*/
30+
indentationSize?: "small" | "medium" | "large";
31+
/**
32+
* How the content box and avatar is aligned.
33+
* If `left` is set then the avatar is on the left side, and the indentation on the right side.
34+
*/
35+
alignment?: "left" | "right";
36+
/**
37+
* If set then the chat bubble only grows to a height of 50% of the viewport.
38+
* In case you need to set other maximum heights then use the `style` property directly.
39+
*/
40+
limitHeight?: boolean;
41+
/**
42+
* If given then the content is automatically parsed and displayed by our `<Markdown />` component.
43+
* `children` need to a `string` then, otherwise it cannot be parsed.
44+
*/
45+
markdownProps?: Omit<MarkdownProps, "children">;
46+
/**
47+
* Could be used to add some type of toggle button or to include a context menu.
48+
*/
49+
actionButton?: React.ReactElement<IconButtonProps> | React.ReactElement<ContextMenuProps>;
50+
}
51+
52+
/**
53+
* Component to display single chat contents, including avatar and status line.
54+
*/
55+
export const ChatContent = ({
56+
className,
57+
children,
58+
statusLine,
59+
avatar,
60+
displayType = "bubble",
61+
indentationSize,
62+
alignment = "left",
63+
limitHeight,
64+
markdownProps,
65+
actionButton,
66+
...otherDivProps
67+
}: ChatContentProps) => {
68+
const content =
69+
markdownProps && typeof children === "string" ? <Markdown {...markdownProps}>{children}</Markdown> : children;
70+
71+
const chatitem = (
72+
<div
73+
className={
74+
`${eccgui}-chat__content` +
75+
` ${eccgui}-chat__content--display-${displayType}` +
76+
` ${eccgui}-chat__content--align-${alignment}` +
77+
(limitHeight ? ` ${eccgui}-chat__content--limitheight` : "") +
78+
(className ? ` ${className}` : "")
79+
}
80+
{...otherDivProps}
81+
>
82+
{statusLine && (
83+
<HtmlContentBlock small>
84+
{statusLine}
85+
<Spacing size="tiny" />
86+
</HtmlContentBlock>
87+
)}
88+
{content}
89+
</div>
90+
);
91+
92+
const indentationSizes = {
93+
small: "8%",
94+
medium: "21%",
95+
large: "34%",
96+
};
97+
98+
return (
99+
<div
100+
style={{
101+
marginLeft: alignment === "right" && indentationSize ? indentationSizes[indentationSize] : undefined,
102+
marginRight: alignment === "left" && indentationSize ? indentationSizes[indentationSize] : undefined,
103+
}}
104+
>
105+
<FlexibleLayoutContainer noEqualItemSpace gapSize="tiny">
106+
{avatar && (
107+
<FlexibleLayoutItem
108+
className={`${eccgui}-chat__content-avatar`}
109+
growFactor={0}
110+
shrinkFactor={0}
111+
style={alignment === "right" ? { order: 1 } : undefined}
112+
>
113+
{React.cloneElement(avatar, { size: "small", ratio: "1:1", rounded: true, resizing: "cover" })}
114+
</FlexibleLayoutItem>
115+
)}
116+
<FlexibleLayoutItem className={`${eccgui}-chat__content-wrapper`}>{chatitem}</FlexibleLayoutItem>
117+
{actionButton && (
118+
<FlexibleLayoutItem
119+
className={`${eccgui}-chat__content-sizetoggle`}
120+
growFactor={0}
121+
shrinkFactor={0}
122+
style={alignment === "right" ? { order: -1 } : undefined}
123+
>
124+
{actionButton}
125+
</FlexibleLayoutItem>
126+
)}
127+
</FlexibleLayoutContainer>
128+
</div>
129+
);
130+
};
131+
132+
export default ChatContent;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from "react";
2+
3+
import { Markdown } from "../../cmem/markdown/Markdown";
4+
import { IconButton } from "../Icon/IconButton";
5+
import { TextReducer, TextReducerProps } from "../TextReducer/TextReducer";
6+
7+
import { ChatContentProps } from "./ChatContent";
8+
9+
export interface ChatContentCollapsedProps {
10+
children: React.ReactElement<ChatContentProps>;
11+
/**
12+
* Set this to `false` if the compoment should initally start in an expanded state.
13+
*/
14+
collapsed?: boolean;
15+
/**
16+
* Use this to set extra `TextReducer` properties.
17+
* This is used to create the collapsed variant of the given content.
18+
*/
19+
textReducerProps?: Omit<TextReducerProps, "children">;
20+
/**
21+
* Text for collapse button.
22+
*/
23+
textCollapse?: string;
24+
/**
25+
* Text for expand button.
26+
*/
27+
textExpand?: string;
28+
}
29+
30+
/**
31+
* Adds an auto collapsing feature for convenience to `ChatContent`.
32+
*/
33+
export const ChatContentCollapsed = ({
34+
children,
35+
collapsed = true,
36+
textReducerProps = {},
37+
textCollapse = "Collapse",
38+
textExpand = "Expand",
39+
}: ChatContentCollapsedProps) => {
40+
const [displayCollapsed, setDispayCollapsed] = React.useState<boolean>(collapsed);
41+
42+
const childrenAsTextline = (
43+
<TextReducer useOverflowTextWrapper {...textReducerProps}>
44+
{typeof children.props.children === "string" ? (
45+
<Markdown>{children.props.children}</Markdown>
46+
) : (
47+
children.props.children
48+
)}
49+
</TextReducer>
50+
);
51+
52+
return React.cloneElement(children, {
53+
children: displayCollapsed ? childrenAsTextline : children.props.children,
54+
actionButton: (
55+
<IconButton
56+
text={displayCollapsed ? textExpand : textCollapse}
57+
name={displayCollapsed ? "toggler-showmore" : "toggler-showless"}
58+
onClick={() => setDispayCollapsed(!displayCollapsed)}
59+
/>
60+
),
61+
});
62+
};
63+
64+
export default ChatContentCollapsed;

0 commit comments

Comments
 (0)