diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 2e5cb5edf..000000000 --- a/package-lock.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "name": "owncast", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "fetch-retry": "^5.0.2", - "isomorphic-fetch": "^3.0.0" - } - }, - "node_modules/fetch-retry": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.2.tgz", - "integrity": "sha512-57Hmu+1kc6pKFUGVIobT7qw3NeAzY/uNN26bSevERLVvf6VGFR/ooDCOFBHMNDgAxBiU2YJq1D0vFzc6U1DcPw==" - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - }, - "dependencies": { - "fetch-retry": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.2.tgz", - "integrity": "sha512-57Hmu+1kc6pKFUGVIobT7qw3NeAzY/uNN26bSevERLVvf6VGFR/ooDCOFBHMNDgAxBiU2YJq1D0vFzc6U1DcPw==" - }, - "isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "requires": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - }, - "whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 65ae8e323..000000000 --- a/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "fetch-retry": "^5.0.2", - "isomorphic-fetch": "^3.0.0" - } -} diff --git a/web/components/chat/ChatContainer/ChatContainer.tsx b/web/components/chat/ChatContainer/ChatContainer.tsx index 3b3ac565e..15585fbee 100644 --- a/web/components/chat/ChatContainer/ChatContainer.tsx +++ b/web/components/chat/ChatContainer/ChatContainer.tsx @@ -1,6 +1,6 @@ import { Spin } from 'antd'; import { Virtuoso } from 'react-virtuoso'; -import { useRef } from 'react'; +import { useMemo, useRef } from 'react'; import { LoadingOutlined } from '@ant-design/icons'; import { MessageType, NameChangeEvent } from '../../../interfaces/socket-events'; @@ -12,10 +12,13 @@ import ChatActionMessage from '../ChatActionMessage'; interface Props { messages: ChatMessage[]; loading: boolean; + usernameToHighlight: string; + chatUserId: string; + isModerator: boolean; } export default function ChatContainer(props: Props) { - const { messages, loading } = props; + const { messages, loading, usernameToHighlight, chatUserId, isModerator } = props; const chatContainerRef = useRef(null); const spinIcon = ; @@ -31,7 +34,14 @@ export default function ChatContainer(props: Props) { const getViewForMessage = message => { switch (message.type) { case MessageType.CHAT: - return ; + return ( + + ); case MessageType.NAME_CHANGE: return getNameChangeViewForMessage(message); default: @@ -39,12 +49,8 @@ export default function ChatContainer(props: Props) { } }; - return ( -
-
- stream chat -
- + const MessagesTable = useMemo( + () => ( getViewForMessage(message)} followOutput="smooth" /> + ), + [messages, usernameToHighlight, chatUserId, isModerator], + ); + + return ( +
+
+ stream chat +
+ + {MessagesTable} +
); } diff --git a/web/components/chat/ChatUserMessage/ChatUserMessage.module.scss b/web/components/chat/ChatUserMessage/ChatUserMessage.module.scss index c411d4eca..d723172c3 100644 --- a/web/components/chat/ChatUserMessage/ChatUserMessage.module.scss +++ b/web/components/chat/ChatUserMessage/ChatUserMessage.module.scss @@ -12,5 +12,20 @@ } .message { color: var(--color-owncast-grey-100); + + mark { + color: white; + padding: 0.1em 0.4em; + border-radius: 0.5em 0.3em; + background: transparent; + background-image: linear-gradient( + to right, + rgba(255, 225, 0, 0.1), + rgba(255, 225, 0, 0.358) 4%, + rgba(255, 225, 0, 0.3) + ); + -webkit-box-decoration-break: clone; + box-decoration-break: clone; + } } } diff --git a/web/components/chat/ChatUserMessage/ChatUserMessage.tsx b/web/components/chat/ChatUserMessage/ChatUserMessage.tsx index 7d1e6c3e6..117e96680 100644 --- a/web/components/chat/ChatUserMessage/ChatUserMessage.tsx +++ b/web/components/chat/ChatUserMessage/ChatUserMessage.tsx @@ -1,15 +1,25 @@ /* eslint-disable react/no-danger */ import { useEffect, useState } from 'react'; +import { Highlight } from 'react-highlighter-ts'; +import he from 'he'; import { ChatMessage } from '../../../interfaces/chat-message.model'; -import { formatTimestamp, formatMessageText } from './messageFmt'; +import { formatTimestamp } from './messageFmt'; import s from './ChatUserMessage.module.scss'; interface Props { message: ChatMessage; showModeratorMenu: boolean; + highlightString: string; + renderAsPersonallySent: boolean; } -export default function ChatUserMessage({ message, showModeratorMenu }: Props) { +export default function ChatUserMessage({ + message, + highlightString, + showModeratorMenu, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + renderAsPersonallySent, // Move the border to the right and render a background +}: Props) { const { body, user, timestamp } = message; const { displayName, displayColor } = user; const color = `hsl(${displayColor}, 100%, 70%)`; @@ -17,7 +27,7 @@ export default function ChatUserMessage({ message, showModeratorMenu }: Props) { const [formattedMessage, setFormattedMessage] = useState(body); useEffect(() => { - setFormattedMessage(formatMessageText(body)); + setFormattedMessage(he.decode(body)); }, [message]); return ( @@ -25,7 +35,9 @@ export default function ChatUserMessage({ message, showModeratorMenu }: Props) {
{displayName}
-
+ +
{formattedMessage}
+
{showModeratorMenu &&
Moderator menu
}
); diff --git a/web/components/stores/ClientConfigStore.tsx b/web/components/stores/ClientConfigStore.tsx index ccd1988af..bffe61893 100644 --- a/web/components/stores/ClientConfigStore.tsx +++ b/web/components/stores/ClientConfigStore.tsx @@ -47,6 +47,16 @@ export const chatDisplayNameAtom = atom({ default: null, }); +export const chatUserIdAtom = atom({ + key: 'chatUserIdAtom', + default: null, +}); + +export const isChatModeratorAtom = atom({ + key: 'isModeratorAtom', + default: false, +}); + export const accessTokenAtom = atom({ key: 'accessTokenAtom', default: null, @@ -135,6 +145,8 @@ export function ClientConfigStore() { const [appState, appStateSend, appStateService] = useMachine(appStateModel); const setChatDisplayName = useSetRecoilState(chatDisplayNameAtom); + const setChatUserId = useSetRecoilState(chatUserIdAtom); + const setIsChatModerator = useSetRecoilState(isChatModeratorAtom); const setClientConfig = useSetRecoilState(clientConfigStateAtom); const setServerStatus = useSetRecoilState(serverStatusState); const setClockSkew = useSetRecoilState(clockSkewAtom); @@ -236,7 +248,12 @@ export function ClientConfigStore() { resetAndReAuth(); break; case MessageType.CONNECTED_USER_INFO: - handleConnectedClientInfoMessage(message as ConnectedClientInfoEvent, setChatDisplayName); + handleConnectedClientInfoMessage( + message as ConnectedClientInfoEvent, + setChatDisplayName, + setChatUserId, + setIsChatModerator, + ); break; case MessageType.CHAT: handleChatMessage(message as ChatEvent, chatMessages, setChatMessages); diff --git a/web/components/stores/eventhandlers/connected-client-info-handler.ts b/web/components/stores/eventhandlers/connected-client-info-handler.ts index 9cce6719d..36ac64060 100644 --- a/web/components/stores/eventhandlers/connected-client-info-handler.ts +++ b/web/components/stores/eventhandlers/connected-client-info-handler.ts @@ -3,8 +3,12 @@ import { ConnectedClientInfoEvent } from '../../../interfaces/socket-events'; export default function handleConnectedClientInfoMessage( message: ConnectedClientInfoEvent, setChatDisplayName: (string) => void, + setChatUserId: (number) => void, + setIsChatModerator: (boolean) => void, ) { const { user } = message; - const { displayName } = user; + const { id, displayName, scopes } = user; setChatDisplayName(displayName); + setChatUserId(id); + setIsChatModerator(scopes.includes('moderator')); } diff --git a/web/components/ui/Content/Content.tsx b/web/components/ui/Content/Content.tsx index 07be48316..e4905aacb 100644 --- a/web/components/ui/Content/Content.tsx +++ b/web/components/ui/Content/Content.tsx @@ -4,6 +4,8 @@ import { Layout, Tabs, Spin } from 'antd'; import { clientConfigStateAtom, chatMessagesAtom, + chatDisplayNameAtom, + chatUserIdAtom, isChatVisibleSelector, serverStatusState, appStateAtom, @@ -43,6 +45,8 @@ export default function ContentComponent() { const isChatVisible = useRecoilValue(isChatVisibleSelector); const messages = useRecoilValue(chatMessagesAtom); const online = useRecoilValue(isOnlineSelector); + const chatDisplayName = useRecoilValue(chatDisplayNameAtom); + const chatUserId = useRecoilValue(chatUserIdAtom); const { extraPageContent, version, socialHandles, name, title, tags, summary } = clientConfig; const { viewerCount, lastConnectTime, lastDisconnectTime } = status; @@ -125,7 +129,13 @@ export default function ContentComponent() { {isChatVisible && (
- +
)} diff --git a/web/components/ui/Sidebar/Sidebar.tsx b/web/components/ui/Sidebar/Sidebar.tsx index 987712ab3..91ae27f74 100644 --- a/web/components/ui/Sidebar/Sidebar.tsx +++ b/web/components/ui/Sidebar/Sidebar.tsx @@ -4,16 +4,29 @@ import { ChatMessage } from '../../../interfaces/chat-message.model'; import { ChatContainer, ChatTextField } from '../../chat'; import s from './Sidebar.module.scss'; -import { chatMessagesAtom, appStateAtom } from '../../stores/ClientConfigStore'; +import { + chatMessagesAtom, + appStateAtom, + chatDisplayNameAtom, + chatUserIdAtom, +} from '../../stores/ClientConfigStore'; import { AppStateOptions } from '../../stores/application-state'; export default function Sidebar() { const messages = useRecoilValue(chatMessagesAtom); const appState = useRecoilValue(appStateAtom); + const chatDisplayName = useRecoilValue(chatDisplayNameAtom); + const chatUserId = useRecoilValue(chatUserIdAtom); return ( - + ); diff --git a/web/package-lock.json b/web/package-lock.json index c114156f9..a11a55eea 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -20,6 +20,7 @@ "chartkick": "4.1.1", "classnames": "2.3.1", "date-fns": "2.28.0", + "he": "^1.2.0", "lodash": "4.17.21", "markdown-it": "12.3.2", "next": "^12.1.5", @@ -34,6 +35,7 @@ "react-contenteditable": "^3.3.6", "react-crossfade-img": "^1.0.0", "react-dom": "17.0.2", + "react-highlighter-ts": "^2.2.0", "react-hotkeys-hook": "^3.4.6", "react-linkify": "1.0.0-alpha", "react-markdown": "8.0.0", @@ -27300,6 +27302,14 @@ "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-highlighter-ts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-highlighter-ts/-/react-highlighter-ts-2.2.0.tgz", + "integrity": "sha512-wZgYrOq6bO1O1szjyduvqeiuV1DuZbKb22FvbclXEhyG5rw9vFaJhdO420RwBvDkD4DvFnDLX4hFuAOg0aQkXA==", + "dependencies": { + "react": "^17.0.1" + } + }, "node_modules/react-hotkeys-hook": { "version": "3.4.6", "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.6.tgz", @@ -53410,6 +53420,14 @@ "shallowequal": "^1.1.0" } }, + "react-highlighter-ts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-highlighter-ts/-/react-highlighter-ts-2.2.0.tgz", + "integrity": "sha512-wZgYrOq6bO1O1szjyduvqeiuV1DuZbKb22FvbclXEhyG5rw9vFaJhdO420RwBvDkD4DvFnDLX4hFuAOg0aQkXA==", + "requires": { + "react": "^17.0.1" + } + }, "react-hotkeys-hook": { "version": "3.4.6", "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.6.tgz", diff --git a/web/package.json b/web/package.json index 72ea18d1c..ac9bd74f4 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,7 @@ "chartkick": "4.1.1", "classnames": "2.3.1", "date-fns": "2.28.0", + "he": "^1.2.0", "lodash": "4.17.21", "markdown-it": "12.3.2", "next": "^12.1.5", @@ -38,6 +39,7 @@ "react-contenteditable": "^3.3.6", "react-crossfade-img": "^1.0.0", "react-dom": "17.0.2", + "react-highlighter-ts": "^2.2.0", "react-hotkeys-hook": "^3.4.6", "react-linkify": "1.0.0-alpha", "react-markdown": "8.0.0", diff --git a/web/stories/ChatContainer.stories.tsx b/web/stories/ChatContainer.stories.tsx index 1a0942d7d..440fbb7ca 100644 --- a/web/stories/ChatContainer.stories.tsx +++ b/web/stories/ChatContainer.stories.tsx @@ -32,7 +32,13 @@ const AddMessagesChatExample = args => { - +
); }; diff --git a/web/stories/ChatUserMessage.stories.tsx b/web/stories/ChatUserMessage.stories.tsx index afd12a7a3..2b6e653bd 100644 --- a/web/stories/ChatUserMessage.stories.tsx +++ b/web/stories/ChatUserMessage.stories.tsx @@ -92,3 +92,10 @@ FromAuthenticatedUser.args = { message: authenticatedUserMessage, showModeratorMenu: false, }; + +export const WithStringHighlighted = Template.bind({}); +WithStringHighlighted.args = { + message: standardMessage, + showModeratorMenu: false, + highlightString: 'message', +};