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',
+};