mirror of
https://github.com/owncast/owncast.git
synced 2024-11-22 21:03:19 +03:00
Use contentEditable for chat input field
This commit is contained in:
parent
008f607cf7
commit
c56c45d904
8 changed files with 112 additions and 31 deletions
|
@ -26,15 +26,14 @@ export default function ChatContainer(props: Props) {
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(messages);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Chat</h1>
|
<h1>Chat</h1>
|
||||||
<Spin spinning={loading} indicator={spinIcon} />
|
<Spin spinning={loading} indicator={spinIcon} />
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
|
style={{ height: '80vh' }}
|
||||||
ref={chatContainerRef}
|
ref={chatContainerRef}
|
||||||
initialTopMostItemIndex={999}
|
initialTopMostItemIndex={999}
|
||||||
data={messages}
|
data={messages}
|
||||||
|
|
|
@ -1,34 +1,42 @@
|
||||||
import { MoreOutlined } from '@ant-design/icons';
|
import { SmileOutlined } from '@ant-design/icons';
|
||||||
import { Input, Button } from 'antd';
|
import { Button, Input } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import ContentEditable from 'react-contenteditable';
|
||||||
|
import WebsocketService from '../../../services/websocket-service';
|
||||||
|
import { websocketServiceAtom } from '../../stores/ClientConfigStore';
|
||||||
import s from './ChatTextField.module.scss';
|
import s from './ChatTextField.module.scss';
|
||||||
|
|
||||||
interface Props {}
|
interface Props {
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function ChatTextField(props: Props) {
|
export default function ChatTextField(props: Props) {
|
||||||
const [value, setValue] = useState('');
|
const { value: originalValue } = props;
|
||||||
|
const [value, setValue] = useState(originalValue);
|
||||||
const [showEmojis, setShowEmojis] = useState(false);
|
const [showEmojis, setShowEmojis] = useState(false);
|
||||||
// large is 40px
|
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||||
const size = 'large';
|
|
||||||
|
|
||||||
useEffect(() => {
|
const text = useRef(value);
|
||||||
console.log({ value });
|
|
||||||
}, [value]);
|
// large is 40px
|
||||||
|
const size = 'small';
|
||||||
|
|
||||||
|
const handleChange = evt => {
|
||||||
|
text.current = evt.target.value;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Input.Group compact style={{ display: 'flex' }}>
|
<Input.Group compact style={{ display: 'flex', width: '100%', position: 'absolute' }}>
|
||||||
<Input
|
<ContentEditable
|
||||||
onChange={e => setValue(e.target.value)}
|
style={{ width: '60%', maxHeight: '50px', padding: '5px' }}
|
||||||
size={size}
|
html={text.current}
|
||||||
placeholder="Enter text and hit enter!"
|
onChange={handleChange}
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size={size}
|
|
||||||
icon={<MoreOutlined />}
|
|
||||||
type="default"
|
|
||||||
onClick={() => setShowEmojis(!showEmojis)}
|
|
||||||
/>
|
/>
|
||||||
|
<Button type="default" ghost title="Emoji" onClick={() => setShowEmojis(!showEmojis)}>
|
||||||
|
<SmileOutlined style={{ color: 'rgba(0,0,0,.45)' }} />
|
||||||
|
</Button>
|
||||||
<Button size={size} type="primary">
|
<Button size={size} type="primary">
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -36,3 +44,7 @@ export default function ChatTextField(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatTextField.defaultProps = {
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
|
|
@ -59,6 +59,11 @@ export const chatMessagesAtom = atom<ChatMessage[]>({
|
||||||
default: [] as ChatMessage[],
|
default: [] as ChatMessage[],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const websocketServiceAtom = atom<WebsocketService>({
|
||||||
|
key: 'websocketServiceAtom',
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
|
||||||
export function ClientConfigStore() {
|
export function ClientConfigStore() {
|
||||||
const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom);
|
const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom);
|
||||||
const setChatVisibility = useSetRecoilState<ChatVisibilityState>(chatVisibilityAtom);
|
const setChatVisibility = useSetRecoilState<ChatVisibilityState>(chatVisibilityAtom);
|
||||||
|
@ -67,8 +72,10 @@ export function ClientConfigStore() {
|
||||||
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
||||||
const [appState, setAppState] = useRecoilState<AppState>(appStateAtom);
|
const [appState, setAppState] = useRecoilState<AppState>(appStateAtom);
|
||||||
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
||||||
|
const [websocketService, setWebsocketService] =
|
||||||
|
useRecoilState<WebsocketService>(websocketServiceAtom);
|
||||||
|
|
||||||
let websocketService: WebsocketService;
|
// let websocketService: WebsocketService;
|
||||||
|
|
||||||
const updateClientConfig = async () => {
|
const updateClientConfig = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -126,8 +133,9 @@ export function ClientConfigStore() {
|
||||||
const startChat = async () => {
|
const startChat = async () => {
|
||||||
setChatState(ChatState.Loading);
|
setChatState(ChatState.Loading);
|
||||||
try {
|
try {
|
||||||
websocketService = new WebsocketService(accessToken, '/ws');
|
const ws = new WebsocketService(accessToken, '/ws');
|
||||||
websocketService.handleMessage = handleMessage;
|
ws.handleMessage = handleMessage;
|
||||||
|
setWebsocketService(ws);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`ChatService -> startChat() ERROR: \n${error}`);
|
console.error(`ChatService -> startChat() ERROR: \n${error}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Layout, Tabs, Layout, Row, Col, Tabs } from 'antd';
|
import { Layout, Row, Col, Tabs } from 'antd';
|
||||||
import Grid from 'antd/lib/card/Grid';
|
import Grid from 'antd/lib/card/Grid';
|
||||||
import {
|
import {
|
||||||
chatVisibilityAtom,
|
chatVisibilityAtom,
|
||||||
|
|
22
web/package-lock.json
generated
22
web/package-lock.json
generated
|
@ -26,6 +26,7 @@
|
||||||
"rc-util": "5.17.0",
|
"rc-util": "5.17.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-chartkick": "0.5.2",
|
"react-chartkick": "0.5.2",
|
||||||
|
"react-contenteditable": "^3.3.6",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-linkify": "1.0.0-alpha",
|
"react-linkify": "1.0.0-alpha",
|
||||||
"react-markdown": "8.0.0",
|
"react-markdown": "8.0.0",
|
||||||
|
@ -26414,6 +26415,18 @@
|
||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-contenteditable": {
|
||||||
|
"version": "3.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.6.tgz",
|
||||||
|
"integrity": "sha512-61+Anbmzggel1sP7nwvxq3d2woD3duR5R89RoLGqKan1A+nruFIcmLjw2F+qqk70AyABls0BDKzE1vqS1UIF1g==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"prop-types": "^15.7.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-docgen": {
|
"node_modules/react-docgen": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.0.tgz",
|
||||||
|
@ -51724,6 +51737,15 @@
|
||||||
"integrity": "sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==",
|
"integrity": "sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"react-contenteditable": {
|
||||||
|
"version": "3.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.6.tgz",
|
||||||
|
"integrity": "sha512-61+Anbmzggel1sP7nwvxq3d2woD3duR5R89RoLGqKan1A+nruFIcmLjw2F+qqk70AyABls0BDKzE1vqS1UIF1g==",
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"prop-types": "^15.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-docgen": {
|
"react-docgen": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.0.tgz",
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"rc-util": "5.17.0",
|
"rc-util": "5.17.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-chartkick": "0.5.2",
|
"react-chartkick": "0.5.2",
|
||||||
|
"react-contenteditable": "^3.3.6",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-linkify": "1.0.0-alpha",
|
"react-linkify": "1.0.0-alpha",
|
||||||
"react-markdown": "8.0.0",
|
"react-markdown": "8.0.0",
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,15 +1,44 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
import ChatTextField from '../components/chat/ChatTextField/ChatTextField';
|
import ChatTextField from '../components/chat/ChatTextField/ChatTextField';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'owncast/Chat/Input text field',
|
title: 'owncast/Chat/Input text field',
|
||||||
component: ChatTextField,
|
component: ChatTextField,
|
||||||
parameters: {},
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
- This is a element using \`contentEditable\` in order to support rendering emoji images inline.
|
||||||
|
- Emoji button shows emoji picker.
|
||||||
|
- Should show one line by default, but grow to two lines as needed.
|
||||||
|
- The Send button should be hidden for desktop layouts and be shown for mobile layouts.`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} as ComponentMeta<typeof ChatTextField>;
|
} as ComponentMeta<typeof ChatTextField>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const Template: ComponentStory<typeof ChatTextField> = args => <ChatTextField />;
|
const Template: ComponentStory<typeof ChatTextField> = args => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<ChatTextField {...args} />
|
||||||
|
</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const Example = Template.bind({});
|
export const Example = Template.bind({});
|
||||||
|
|
||||||
|
export const LongerMessage = Template.bind({});
|
||||||
|
LongerMessage.args = {
|
||||||
|
value:
|
||||||
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
|
||||||
|
};
|
||||||
|
|
||||||
|
LongerMessage.parameters = {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: 'Should display two lines of text and scroll to display more.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue