diff --git a/web/components/main-layout.tsx b/web/components/main-layout.tsx index 37bac402c..80a617bd9 100644 --- a/web/components/main-layout.tsx +++ b/web/components/main-layout.tsx @@ -16,6 +16,7 @@ import { QuestionCircleOutlined, MessageOutlined, ExperimentOutlined, + UserOutlined, } from '@ant-design/icons'; import classNames from 'classnames'; import { upgradeVersionAvailable } from '../utils/apis'; @@ -78,6 +79,8 @@ export default function MainLayout(props) { const upgradeVersionString = `${upgradeVersion}` || ''; const upgradeMessage = `Upgrade to v${upgradeVersionString}`; + const chatMenuItemStyle = 'block'; //upgradeVersion ? 'block' : 'none'; + const clearAlertMessage = () => { alertMessage.setMessage(null); }; @@ -144,9 +147,21 @@ export default function MainLayout(props) { Viewers - } title="Chat utilities"> - Chat - + } + style={{ display: chatMenuItemStyle }} + > + } title="Chat utilities"> + Messages + + + } title="Chat utilities"> + Users + + + }> @@ -159,6 +174,9 @@ export default function MainLayout(props) { Video Configuration + + Chat + Storage diff --git a/web/components/message-visiblity-toggle.tsx b/web/components/message-visiblity-toggle.tsx index 0e428e812..cd93c19f2 100644 --- a/web/components/message-visiblity-toggle.tsx +++ b/web/components/message-visiblity-toggle.tsx @@ -9,7 +9,7 @@ import { } from '@ant-design/icons'; import { fetchData, UPDATE_CHAT_MESSGAE_VIZ } from '../utils/apis'; import { MessageType } from '../types/chat'; -import { OUTCOME_TIMEOUT } from '../pages/chat'; +import { OUTCOME_TIMEOUT } from '../pages/chat/messages'; import { isEmptyObject } from '../utils/format'; interface MessageToggleProps { diff --git a/web/pages/chat.tsx b/web/pages/chat/messages.tsx similarity index 96% rename from web/pages/chat.tsx rename to web/pages/chat/messages.tsx index 7d01e373e..5916bf11b 100644 --- a/web/pages/chat.tsx +++ b/web/pages/chat/messages.tsx @@ -5,10 +5,10 @@ import classNames from 'classnames'; import { ColumnsType } from 'antd/es/table'; import format from 'date-fns/format'; -import { CHAT_HISTORY, fetchData, FETCH_INTERVAL, UPDATE_CHAT_MESSGAE_VIZ } from '../utils/apis'; -import { MessageType } from '../types/chat'; -import { isEmptyObject } from '../utils/format'; -import MessageVisiblityToggle from '../components/message-visiblity-toggle'; +import { CHAT_HISTORY, fetchData, FETCH_INTERVAL, UPDATE_CHAT_MESSGAE_VIZ } from '../../utils/apis'; +import { MessageType } from '../../types/chat'; +import { isEmptyObject } from '../../utils/format'; +import MessageVisiblityToggle from '../../components/message-visiblity-toggle'; const { Title } = Typography; diff --git a/web/pages/chat/users.tsx b/web/pages/chat/users.tsx new file mode 100644 index 000000000..d340be472 --- /dev/null +++ b/web/pages/chat/users.tsx @@ -0,0 +1,114 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { Table, Typography } from 'antd'; +import { formatDistanceToNow } from 'date-fns'; +import { SortOrder } from 'antd/lib/table/interface'; + +import { ServerStatusContext } from '../../utils/server-status-context'; + +import { CONNECTED_CLIENTS, VIEWERS_OVER_TIME, fetchData } from '../../utils/apis'; + +const FETCH_INTERVAL = 60 * 1000; // 1 min + +export default function ChatUsers() { + const context = useContext(ServerStatusContext); + const { online } = context || {}; + + const [viewerInfo, setViewerInfo] = useState([]); + const [clients, setClients] = useState([]); + + const getInfo = async () => { + try { + const result = await fetchData(VIEWERS_OVER_TIME); + setViewerInfo(result); + } catch (error) { + console.log('==== error', error); + } + + try { + const result = await fetchData(CONNECTED_CLIENTS); + setClients(result); + } catch (error) { + console.log('==== error', error); + } + }; + + useEffect(() => { + let getStatusIntervalId = null; + + getInfo(); + if (online) { + getStatusIntervalId = setInterval(getInfo, FETCH_INTERVAL); + // returned function will be called on component unmount + return () => { + clearInterval(getStatusIntervalId); + }; + } + + return () => []; + }, [online]); + + // todo - check to see if broadcast active has changed. if so, start polling. + + if (!viewerInfo.length) { + return 'no info'; + } + + const columns = [ + { + title: 'User name', + dataIndex: 'username', + key: 'username', + render: username => username || '-', + sorter: (a, b) => a.username - b.username, + sortDirections: ['descend', 'ascend'] as SortOrder[], + }, + { + title: 'Messages sent', + dataIndex: 'messageCount', + key: 'messageCount', + sorter: (a, b) => a.messageCount - b.messageCount, + sortDirections: ['descend', 'ascend'] as SortOrder[], + }, + { + title: 'Connected Time', + dataIndex: 'connectedAt', + key: 'connectedAt', + render: time => formatDistanceToNow(new Date(time)), + sorter: (a, b) => new Date(a.connectedAt).getTime() - new Date(b.connectedAt).getTime(), + sortDirections: ['descend', 'ascend'] as SortOrder[], + }, + { + title: 'User Agent', + dataIndex: 'userAgent', + key: 'userAgent', + }, + { + title: 'Location', + dataIndex: 'geo', + key: 'geo', + render: geo => (geo ? `${geo.regionName}, ${geo.countryCode}` : '-'), + }, + ]; + + return ( + <> +
+ Connected + row.clientID} /> +

+ + Visit the{' '} + + documentation + {' '} + to configure additional details about your viewers. + {' '} +

+ + + ); +} diff --git a/web/pages/config-chat.tsx b/web/pages/config-chat.tsx new file mode 100644 index 000000000..351cd3398 --- /dev/null +++ b/web/pages/config-chat.tsx @@ -0,0 +1,69 @@ +import React, { useState, useContext, useEffect } from 'react'; +import { Typography } from 'antd'; +import { + FIELD_PROPS_DISABLE_CHAT, + TEXTFIELD_PROPS_CHAT_USERNAME_BLOCKLIST, +} from '../utils/config-constants'; +import { ServerStatusContext } from '../utils/server-status-context'; +import ToggleSwitch from '../components/config/form-toggleswitch'; +import { UpdateArgs } from '../types/config-section'; +import { TEXTFIELD_TYPE_TEXTAREA } from '../components/config/form-textfield'; +import TextFieldWithSubmit from '../components/config/form-textfield-with-submit'; + +export default function ConfigChat() { + const { Title } = Typography; + const [formDataValues, setFormDataValues] = useState(null); + const serverStatusData = useContext(ServerStatusContext); + + const { serverConfig } = serverStatusData || {}; + const { chatDisabled } = serverConfig; + const { usernameBlocklist } = serverConfig; + + function handleChatDisableChange(disabled: boolean) { + handleFieldChange({ fieldName: 'chatDisabled', value: disabled }); + } + + function handleChatUsernameBlockListChange(args: UpdateArgs) { + handleFieldChange({ fieldName: 'usernameBlocklist', value: args.value }); + } + + useEffect(() => { + setFormDataValues({ + chatDisabled, + usernameBlocklist, + }); + }, [serverConfig]); + + if (!formDataValues) { + return null; + } + + const handleFieldChange = ({ fieldName, value }: UpdateArgs) => { + setFormDataValues({ + ...formDataValues, + [fieldName]: value, + }); + }; + + return ( +
+ Chat Settings +
+ + +
+
+ ); +} diff --git a/web/types/config-section.ts b/web/types/config-section.ts index e18d4bcc4..5b6fd511f 100644 --- a/web/types/config-section.ts +++ b/web/types/config-section.ts @@ -101,4 +101,5 @@ export interface ConfigDetails { yp: ConfigDirectoryFields; supportedCodecs: string[]; videoCodec: string; + usernameBlocklist: string; } diff --git a/web/utils/config-constants.tsx b/web/utils/config-constants.tsx index c5015f1cc..2ff13482b 100644 --- a/web/utils/config-constants.tsx +++ b/web/utils/config-constants.tsx @@ -30,6 +30,7 @@ export const API_VIDEO_VARIANTS = '/video/streamoutputvariants'; export const API_WEB_PORT = '/webserverport'; export const API_YP_SWITCH = '/directoryenabled'; export const API_CHAT_DISABLE = '/chat/disable'; +export const API_CHAT_USERNAME_BLOCKLIST = '/chat/disallowedusernames'; export const API_EXTERNAL_ACTIONS = '/externalactions'; export const API_VIDEO_CODEC = '/video/codec'; @@ -175,11 +176,18 @@ export const DEFAULT_VARIANT_STATE: VideoVariant = { export const FIELD_PROPS_DISABLE_CHAT = { apiPath: API_CHAT_DISABLE, configPath: 'chatDisabled', - label: 'Hide chat', - tip: 'Hide the web chat interface.', + label: 'Disable chat', + tip: 'Disable chat functionality from your Owncast server.', useSubmit: true, }; +export const TEXTFIELD_PROPS_CHAT_USERNAME_BLOCKLIST = { + apiPath: API_CHAT_USERNAME_BLOCKLIST, + placeholder: 'admin, god, owncast, stewiegriffin', + label: 'Disallowed usernames', + tip: 'A comma seperated list of chat usernames you disallow.', +}; + export const VIDEO_VARIANT_SETTING_DEFAULTS = { // this one is currently unused audioBitrate: { diff --git a/web/utils/server-status-context.tsx b/web/utils/server-status-context.tsx index 9e61d13d7..cca2b135e 100644 --- a/web/utils/server-status-context.tsx +++ b/web/utils/server-status-context.tsx @@ -48,6 +48,7 @@ export const initialServerConfigState: ConfigDetails = { externalActions: [], supportedCodecs: [], videoCodec: '', + usernameBlocklist: '', }; const initialServerStatusState = {