mirror of
https://github.com/owncast/owncast.git
synced 2024-11-25 06:12:23 +03:00
WIP chat user auto complete
This commit is contained in:
parent
4c873d1ac2
commit
95c9239814
8 changed files with 219 additions and 29 deletions
|
@ -30,6 +30,7 @@ export type ChatContainerProps = {
|
||||||
height?: string;
|
height?: string;
|
||||||
chatAvailable: boolean;
|
chatAvailable: boolean;
|
||||||
focusInput?: boolean;
|
focusInput?: boolean;
|
||||||
|
knownChatUserDisplayNames?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function shouldCollapseMessages(
|
function shouldCollapseMessages(
|
||||||
|
@ -92,6 +93,7 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||||
isModerator,
|
isModerator,
|
||||||
showInput,
|
showInput,
|
||||||
height,
|
height,
|
||||||
|
knownChatUserDisplayNames,
|
||||||
chatAvailable: chatEnabled,
|
chatAvailable: chatEnabled,
|
||||||
focusInput = true,
|
focusInput = true,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -285,7 +287,11 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||||
{MessagesTable}
|
{MessagesTable}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<div className={styles.chatTextField}>
|
<div className={styles.chatTextField}>
|
||||||
<ChatTextField enabled={chatEnabled} focusInput={focusInput} />
|
<ChatTextField
|
||||||
|
enabled={chatEnabled}
|
||||||
|
knownChatUserDisplayNames={knownChatUserDisplayNames}
|
||||||
|
focusInput={focusInput}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
box-shadow: 0;
|
box-shadow: 0;
|
||||||
transition: box-shadow 50ms ease-in-out;
|
transition: box-shadow 50ms ease-in-out;
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 1px solid var(--color-owncast-gray-500) !important;
|
outline: 1px solid var(--theme-color-components-form-field-background) !important;
|
||||||
}
|
}
|
||||||
& > p {
|
& > p {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 .25rem;
|
padding: 0 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sendButton {
|
.sendButton {
|
||||||
|
@ -63,3 +63,32 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.userMention {
|
||||||
|
display: inline;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 3px;
|
||||||
|
margin-right: 2px;
|
||||||
|
margin-left: 2px;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--theme-color-components-form-field-text);
|
||||||
|
border: 0.05rem solid var(--theme-color-components-form-field-border);
|
||||||
|
background-color: var(--theme-color-components-form-field-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocompleteSelectMenu {
|
||||||
|
width: 120px;
|
||||||
|
right: '5px';
|
||||||
|
bottom: '5px';
|
||||||
|
z-index: 99999;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-select-selection-item {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-dropdown {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Popover } from 'antd';
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
import React, { FC, useMemo, useState } from 'react';
|
import { Popover, Select } from 'antd';
|
||||||
|
import React, { FC, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Transforms, createEditor, BaseEditor, Text, Descendant, Editor } from 'slate';
|
import { Transforms, createEditor, BaseEditor, Text, Descendant, Editor } from 'slate';
|
||||||
import {
|
import {
|
||||||
|
@ -32,7 +33,10 @@ const SmileOutlined = dynamic(() => import('@ant-design/icons/SmileOutlined'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
type CustomElement = { type: 'paragraph' | 'span'; children: CustomText[] } | ImageNode;
|
type CustomElement =
|
||||||
|
| { type: 'paragraph' | 'span'; children: CustomText[] }
|
||||||
|
| ImageNode
|
||||||
|
| MentionElement;
|
||||||
type CustomText = { text: string };
|
type CustomText = { text: string };
|
||||||
|
|
||||||
type EmptyText = {
|
type EmptyText = {
|
||||||
|
@ -47,6 +51,12 @@ type ImageNode = {
|
||||||
children: EmptyText[];
|
children: EmptyText[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MentionElement = {
|
||||||
|
type: 'mention';
|
||||||
|
name: string;
|
||||||
|
children: CustomText[];
|
||||||
|
};
|
||||||
|
|
||||||
declare module 'slate' {
|
declare module 'slate' {
|
||||||
interface CustomTypes {
|
interface CustomTypes {
|
||||||
Editor: BaseEditor & ReactEditor;
|
Editor: BaseEditor & ReactEditor;
|
||||||
|
@ -78,6 +88,17 @@ const Image = p => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Mention = p => {
|
||||||
|
const { attributes, element, children } = p;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span {...attributes} className={styles.userMention} contentEditable={false}>
|
||||||
|
@{element.name}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const withImages = editor => {
|
const withImages = editor => {
|
||||||
const { isVoid } = editor;
|
const { isVoid } = editor;
|
||||||
|
|
||||||
|
@ -89,6 +110,17 @@ const withImages = editor => {
|
||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const withMentions = editor => {
|
||||||
|
const { isInline, isVoid } = editor;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
editor.isInline = element => (element.type === 'mention' ? true : isInline(element));
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
editor.isVoid = element => (element.type === 'mention' ? true : isVoid(element));
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
const serialize = node => {
|
const serialize = node => {
|
||||||
if (Text.isText(node)) {
|
if (Text.isText(node)) {
|
||||||
const string = node.text;
|
const string = node.text;
|
||||||
|
@ -107,6 +139,8 @@ const serialize = node => {
|
||||||
return `<p>${children}</p>`;
|
return `<p>${children}</p>`;
|
||||||
case 'image':
|
case 'image':
|
||||||
return `<img src="${node.src}" alt="${node.alt}" title="${node.name}" class="emoji"/>`;
|
return `<img src="${node.src}" alt="${node.alt}" title="${node.name}" class="emoji"/>`;
|
||||||
|
case 'mention':
|
||||||
|
return `@${node.name} `;
|
||||||
default:
|
default:
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
@ -116,6 +150,8 @@ const getCharacterCount = node => {
|
||||||
if (Text.isText(node)) {
|
if (Text.isText(node)) {
|
||||||
return node.text.length;
|
return node.text.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hard code each image to count as 5 characters.
|
||||||
if (node.type === 'image') {
|
if (node.type === 'image') {
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
@ -132,15 +168,29 @@ export type ChatTextFieldProps = {
|
||||||
defaultText?: string;
|
defaultText?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
focusInput: boolean;
|
focusInput: boolean;
|
||||||
|
knownChatUserDisplayNames?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const characterLimit = 300;
|
const characterLimit = 300;
|
||||||
|
|
||||||
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
|
export const ChatTextField: FC<ChatTextFieldProps> = ({
|
||||||
|
defaultText,
|
||||||
|
enabled,
|
||||||
|
focusInput,
|
||||||
|
knownChatUserDisplayNames,
|
||||||
|
}) => {
|
||||||
const [showEmojis, setShowEmojis] = useState(false);
|
const [showEmojis, setShowEmojis] = useState(false);
|
||||||
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
||||||
|
const [showingAutoCompleteMenu, setShowingAutoCompleteMenu] = useState(false);
|
||||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||||
const editor = useMemo(() => withReact(withImages(createEditor())), []);
|
const editor = useMemo(() => withReact(withMentions(withImages(createEditor()))), []);
|
||||||
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const chatUserNames = knownChatUserDisplayNames
|
||||||
|
?.filter(c => c.toLowerCase().startsWith(search.toLowerCase()))
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
const defaultEditorValue: Descendant[] = [
|
const defaultEditorValue: Descendant[] = [
|
||||||
{
|
{
|
||||||
|
@ -187,6 +237,16 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
||||||
Editor.normalize(editor, { force: true });
|
Editor.normalize(editor, { force: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const insertMention = (e, chatDisplayName) => {
|
||||||
|
const mention: MentionElement = {
|
||||||
|
type: 'mention',
|
||||||
|
name: chatDisplayName,
|
||||||
|
children: [{ text: '' }],
|
||||||
|
};
|
||||||
|
Transforms.insertNodes(e, mention);
|
||||||
|
Transforms.move(e);
|
||||||
|
};
|
||||||
|
|
||||||
// Native emoji
|
// Native emoji
|
||||||
const onEmojiSelect = (emoji: string) => {
|
const onEmojiSelect = (emoji: string) => {
|
||||||
ReactEditor.focus(editor);
|
ReactEditor.focus(editor);
|
||||||
|
@ -198,7 +258,8 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
||||||
insertImage(emoji, name);
|
insertImage(emoji, name);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
const onKeyDown = useCallback(
|
||||||
|
e => {
|
||||||
const charCount = getCharacterCount(editor) + 1;
|
const charCount = getCharacterCount(editor) + 1;
|
||||||
|
|
||||||
// Send the message when hitting enter.
|
// Send the message when hitting enter.
|
||||||
|
@ -220,7 +281,9 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
||||||
}
|
}
|
||||||
|
|
||||||
setCharacterCount(charCount + 1);
|
setCharacterCount(charCount + 1);
|
||||||
};
|
},
|
||||||
|
[editor],
|
||||||
|
);
|
||||||
|
|
||||||
const onPaste = (e: React.ClipboardEvent) => {
|
const onPaste = (e: React.ClipboardEvent) => {
|
||||||
const text = e.clipboardData.getData('text/plain');
|
const text = e.clipboardData.getData('text/plain');
|
||||||
|
@ -235,6 +298,10 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
||||||
switch (p.element.type) {
|
switch (p.element.type) {
|
||||||
case 'image':
|
case 'image':
|
||||||
return <Image {...p} />;
|
return <Image {...p} />;
|
||||||
|
|
||||||
|
case 'mention':
|
||||||
|
return <Mention {...p} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <p {...p} />;
|
return <p {...p} />;
|
||||||
}
|
}
|
||||||
|
@ -243,12 +310,36 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
||||||
return (
|
return (
|
||||||
<div id="chat-input" className={styles.root}>
|
<div id="chat-input" className={styles.root}>
|
||||||
<div
|
<div
|
||||||
|
ref={inputRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.inputWrap,
|
styles.inputWrap,
|
||||||
characterCount >= characterLimit && styles.maxCharacters,
|
characterCount >= characterLimit && styles.maxCharacters,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Slate editor={editor} initialValue={defaultEditorValue}>
|
<Slate
|
||||||
|
editor={editor}
|
||||||
|
initialValue={defaultEditorValue}
|
||||||
|
onChange={() => {
|
||||||
|
const { selection } = editor;
|
||||||
|
|
||||||
|
if (selection && Range.isCollapsed(selection)) {
|
||||||
|
const [start] = Range.edges(selection);
|
||||||
|
const wordBefore = Editor.before(editor, start, { unit: 'word' });
|
||||||
|
const before = wordBefore && Editor.before(editor, wordBefore);
|
||||||
|
const beforeRange = before && Editor.range(editor, before, start);
|
||||||
|
const beforeText = beforeRange && Editor.string(editor, beforeRange);
|
||||||
|
const beforeMatch = beforeText && beforeText.match(/^@(\w+)$/);
|
||||||
|
const after = Editor.after(editor, start);
|
||||||
|
const afterRange = Editor.range(editor, start, after);
|
||||||
|
const afterText = Editor.string(editor, afterRange);
|
||||||
|
const afterMatch = afterText.match(/^(\s|$)/);
|
||||||
|
|
||||||
|
if (beforeMatch && afterMatch) {
|
||||||
|
setShowingAutoCompleteMenu(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Editable
|
<Editable
|
||||||
className="chat-text-input"
|
className="chat-text-input"
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
@ -284,6 +375,34 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
||||||
onOpenChange={open => setShowEmojis(open)}
|
onOpenChange={open => setShowEmojis(open)}
|
||||||
open={showEmojis}
|
open={showEmojis}
|
||||||
/>
|
/>
|
||||||
|
{showingAutoCompleteMenu && (
|
||||||
|
<Select
|
||||||
|
defaultOpen
|
||||||
|
open
|
||||||
|
showArrow={false}
|
||||||
|
bordered={false}
|
||||||
|
dropdownMatchSelectWidth={false}
|
||||||
|
placement="topRight"
|
||||||
|
className={styles.autocompleteSelectMenu}
|
||||||
|
options={chatUserNames?.map(char => ({
|
||||||
|
value: char,
|
||||||
|
label: char,
|
||||||
|
}))}
|
||||||
|
onSelect={value => {
|
||||||
|
// Transforms.select(editor, target);
|
||||||
|
insertMention(editor, value);
|
||||||
|
setShowingAutoCompleteMenu(false);
|
||||||
|
}}
|
||||||
|
onInputKeyDown={e => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setShowingAutoCompleteMenu(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
// style={{ zIndex: 9999 }}
|
||||||
|
getPopupContainer={() => inputRef.current}
|
||||||
|
onBlur={() => setShowingAutoCompleteMenu(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Slate>
|
</Slate>
|
||||||
|
|
||||||
{enabled && (
|
{enabled && (
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable no-case-declarations */
|
||||||
import { FC, useContext, useEffect, useState } from 'react';
|
import { FC, useContext, useEffect, useState } from 'react';
|
||||||
import { atom, selector, useRecoilState, useSetRecoilState, RecoilEnv } from 'recoil';
|
import { atom, selector, useRecoilState, useSetRecoilState, RecoilEnv } from 'recoil';
|
||||||
import { useMachine } from '@xstate/react';
|
import { useMachine } from '@xstate/react';
|
||||||
|
@ -113,6 +114,11 @@ const removedMessageIdsAtom = atom<string[]>({
|
||||||
default: [],
|
default: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const knownChatUserDisplayNamesAtom = atom<string[]>({
|
||||||
|
key: 'knownChatUserDisplayNames',
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
|
|
||||||
export const isChatAvailableSelector = selector({
|
export const isChatAvailableSelector = selector({
|
||||||
key: 'isChatAvailableSelector',
|
key: 'isChatAvailableSelector',
|
||||||
get: ({ get }) => {
|
get: ({ get }) => {
|
||||||
|
@ -171,6 +177,7 @@ export const ClientConfigStore: FC = () => {
|
||||||
const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom);
|
const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom);
|
||||||
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
|
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
|
||||||
const [hiddenMessageIds, setHiddenMessageIds] = useRecoilState<string[]>(removedMessageIdsAtom);
|
const [hiddenMessageIds, setHiddenMessageIds] = useRecoilState<string[]>(removedMessageIdsAtom);
|
||||||
|
const setKnownChatUserDisplayNames = useSetRecoilState<string[]>(knownChatUserDisplayNamesAtom);
|
||||||
const [hasLoadedConfig, setHasLoadedConfig] = useState(false);
|
const [hasLoadedConfig, setHasLoadedConfig] = useState(false);
|
||||||
|
|
||||||
let ws: WebsocketService;
|
let ws: WebsocketService;
|
||||||
|
@ -291,6 +298,10 @@ export const ClientConfigStore: FC = () => {
|
||||||
hasWebsocketDisconnected = false;
|
hasWebsocketDisconnected = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChatUserDisplayNameAdd = (displayName: string) => {
|
||||||
|
setKnownChatUserDisplayNames(currentState => [...currentState, displayName]);
|
||||||
|
};
|
||||||
|
|
||||||
const handleMessage = (message: SocketEvent) => {
|
const handleMessage = (message: SocketEvent) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case MessageType.ERROR_NEEDS_REGISTRATION:
|
case MessageType.ERROR_NEEDS_REGISTRATION:
|
||||||
|
@ -304,6 +315,7 @@ export const ClientConfigStore: FC = () => {
|
||||||
);
|
);
|
||||||
if (message as ChatEvent) {
|
if (message as ChatEvent) {
|
||||||
const m = new ChatEvent(message);
|
const m = new ChatEvent(message);
|
||||||
|
handleChatUserDisplayNameAdd(m.user.displayName);
|
||||||
if (!hasBeenModeratorNotified && m.user?.isModerator) {
|
if (!hasBeenModeratorNotified && m.user?.isModerator) {
|
||||||
setChatMessages(currentState => [...currentState, message as ChatEvent]);
|
setChatMessages(currentState => [...currentState, message as ChatEvent]);
|
||||||
hasBeenModeratorNotified = true;
|
hasBeenModeratorNotified = true;
|
||||||
|
@ -313,6 +325,8 @@ export const ClientConfigStore: FC = () => {
|
||||||
break;
|
break;
|
||||||
case MessageType.CHAT:
|
case MessageType.CHAT:
|
||||||
setChatMessages(currentState => [...currentState, message as ChatEvent]);
|
setChatMessages(currentState => [...currentState, message as ChatEvent]);
|
||||||
|
const m = new ChatEvent(message);
|
||||||
|
handleChatUserDisplayNameAdd(m.user.displayName);
|
||||||
break;
|
break;
|
||||||
case MessageType.NAME_CHANGE:
|
case MessageType.NAME_CHANGE:
|
||||||
handleNameChangeEvent(message as ChatEvent, setChatMessages);
|
handleNameChangeEvent(message as ChatEvent, setChatMessages);
|
||||||
|
|
|
@ -17,7 +17,11 @@ import {
|
||||||
isOnlineSelector,
|
isOnlineSelector,
|
||||||
isMobileAtom,
|
isMobileAtom,
|
||||||
serverStatusState,
|
serverStatusState,
|
||||||
|
<<<<<<< HEAD
|
||||||
isChatAvailableSelector,
|
isChatAvailableSelector,
|
||||||
|
=======
|
||||||
|
knownChatUserDisplayNamesAtom,
|
||||||
|
>>>>>>> 490f5dc79 (WIP chat user auto complete)
|
||||||
} from '../../stores/ClientConfigStore';
|
} from '../../stores/ClientConfigStore';
|
||||||
import { ClientConfig } from '../../../interfaces/client-config.model';
|
import { ClientConfig } from '../../../interfaces/client-config.model';
|
||||||
|
|
||||||
|
@ -98,7 +102,11 @@ export const Content: FC = () => {
|
||||||
const [isMobile, setIsMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
|
const [isMobile, setIsMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
|
||||||
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
|
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
|
||||||
const online = useRecoilValue<boolean>(isOnlineSelector);
|
const online = useRecoilValue<boolean>(isOnlineSelector);
|
||||||
|
<<<<<<< HEAD
|
||||||
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
|
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
|
||||||
|
=======
|
||||||
|
const knownChatUserDisplayNames = useRecoilValue(knownChatUserDisplayNamesAtom);
|
||||||
|
>>>>>>> 490f5dc79 (WIP chat user auto complete)
|
||||||
|
|
||||||
const { viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } =
|
const { viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } =
|
||||||
useRecoilValue<ServerStatus>(serverStatusState);
|
useRecoilValue<ServerStatus>(serverStatusState);
|
||||||
|
@ -268,7 +276,12 @@ export const Content: FC = () => {
|
||||||
extraPageContent={extraPageContent}
|
extraPageContent={extraPageContent}
|
||||||
setShowFollowModal={setShowFollowModal}
|
setShowFollowModal={setShowFollowModal}
|
||||||
supportFediverseFeatures={supportFediverseFeatures}
|
supportFediverseFeatures={supportFediverseFeatures}
|
||||||
|
<<<<<<< HEAD
|
||||||
online={online}
|
online={online}
|
||||||
|
=======
|
||||||
|
chatEnabled={isChatAvailable}
|
||||||
|
knownChatUserDisplayNames={knownChatUserDisplayNames}
|
||||||
|
>>>>>>> 490f5dc79 (WIP chat user auto complete)
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Col span={24} style={{ paddingRight: dynamicPadding }}>
|
<Col span={24} style={{ paddingRight: dynamicPadding }}>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import styles from './Sidebar.module.scss';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
currentUserAtom,
|
currentUserAtom,
|
||||||
|
knownChatUserDisplayNamesAtom,
|
||||||
visibleChatMessagesSelector,
|
visibleChatMessagesSelector,
|
||||||
isChatAvailableSelector,
|
isChatAvailableSelector,
|
||||||
} from '../../stores/ClientConfigStore';
|
} from '../../stores/ClientConfigStore';
|
||||||
|
@ -24,6 +25,7 @@ export const Sidebar: FC = () => {
|
||||||
const currentUser = useRecoilValue(currentUserAtom);
|
const currentUser = useRecoilValue(currentUserAtom);
|
||||||
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
|
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
|
||||||
const isChatAvailable = useRecoilValue(isChatAvailableSelector);
|
const isChatAvailable = useRecoilValue(isChatAvailableSelector);
|
||||||
|
const knownChatUserDisplayNames = useRecoilValue(knownChatUserDisplayNamesAtom);
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
return (
|
return (
|
||||||
|
@ -43,6 +45,7 @@ export const Sidebar: FC = () => {
|
||||||
isModerator={isModerator}
|
isModerator={isModerator}
|
||||||
chatAvailable={isChatAvailable}
|
chatAvailable={isChatAvailable}
|
||||||
showInput={!!currentUser}
|
showInput={!!currentUser}
|
||||||
|
knownChatUserDisplayNames={knownChatUserDisplayNames}
|
||||||
/>
|
/>
|
||||||
</Sider>
|
</Sider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ChatContainer } from '../../../../components/chat/ChatContainer/ChatCon
|
||||||
import {
|
import {
|
||||||
ClientConfigStore,
|
ClientConfigStore,
|
||||||
currentUserAtom,
|
currentUserAtom,
|
||||||
|
knownChatUserDisplayNamesAtom,
|
||||||
visibleChatMessagesSelector,
|
visibleChatMessagesSelector,
|
||||||
isChatAvailableSelector,
|
isChatAvailableSelector,
|
||||||
} from '../../../../components/stores/ClientConfigStore';
|
} from '../../../../components/stores/ClientConfigStore';
|
||||||
|
@ -15,6 +16,7 @@ export default function ReadOnlyChatEmbed() {
|
||||||
const currentUser = useRecoilValue(currentUserAtom);
|
const currentUser = useRecoilValue(currentUserAtom);
|
||||||
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
|
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
|
||||||
const isChatAvailable = useRecoilValue(isChatAvailableSelector);
|
const isChatAvailable = useRecoilValue(isChatAvailableSelector);
|
||||||
|
const knownChatUserDisplayNames = useRecoilValue(knownChatUserDisplayNamesAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -35,6 +37,7 @@ export default function ReadOnlyChatEmbed() {
|
||||||
showInput={false}
|
showInput={false}
|
||||||
height="100vh"
|
height="100vh"
|
||||||
chatAvailable={isChatAvailable}
|
chatAvailable={isChatAvailable}
|
||||||
|
knownChatUserDisplayNames={knownChatUserDisplayNames}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
visibleChatMessagesSelector,
|
visibleChatMessagesSelector,
|
||||||
clientConfigStateAtom,
|
clientConfigStateAtom,
|
||||||
appStateAtom,
|
appStateAtom,
|
||||||
|
knownChatUserDisplayNamesAtom,
|
||||||
serverStatusState,
|
serverStatusState,
|
||||||
isChatAvailableSelector,
|
isChatAvailableSelector,
|
||||||
} from '../../../../components/stores/ClientConfigStore';
|
} from '../../../../components/stores/ClientConfigStore';
|
||||||
|
@ -28,6 +29,7 @@ export default function ReadWriteChatEmbed() {
|
||||||
|
|
||||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||||
const isChatAvailable = useRecoilValue(isChatAvailableSelector);
|
const isChatAvailable = useRecoilValue(isChatAvailableSelector);
|
||||||
|
const knownChatUserDisplayNames = useRecoilValue(knownChatUserDisplayNamesAtom);
|
||||||
|
|
||||||
const { name, chatDisabled } = clientConfig;
|
const { name, chatDisabled } = clientConfig;
|
||||||
const { videoAvailable } = appState;
|
const { videoAvailable } = appState;
|
||||||
|
@ -73,6 +75,7 @@ export default function ReadWriteChatEmbed() {
|
||||||
showInput
|
showInput
|
||||||
height="92vh"
|
height="92vh"
|
||||||
chatAvailable={isChatAvailable}
|
chatAvailable={isChatAvailable}
|
||||||
|
knownChatUserDisplayNames={knownChatUserDisplayNames}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in a new issue