diff --git a/web/components/config/notification/browser.tsx b/web/components/config/notification/browser.tsx new file mode 100644 index 000000000..c84229dd1 --- /dev/null +++ b/web/components/config/notification/browser.tsx @@ -0,0 +1,129 @@ +import { Button, Typography } from 'antd'; +import React, { useState, useContext, useEffect } from 'react'; +import { ServerStatusContext } from '../../../utils/server-status-context'; +import TextField, { TEXTFIELD_TYPE_TEXTAREA } from '../form-textfield'; +import { + postConfigUpdateToAPI, + RESET_TIMEOUT, + BROWSER_PUSH_CONFIG_FIELDS, +} from '../../../utils/config-constants'; +import ToggleSwitch from '../form-toggleswitch'; +import { + createInputStatus, + StatusState, + STATUS_ERROR, + STATUS_SUCCESS, +} from '../../../utils/input-statuses'; +import { UpdateArgs } from '../../../types/config-section'; +import FormStatusIndicator from '../form-status-indicator'; + +const { Title } = Typography; + +export default function ConfigNotify() { + const serverStatusData = useContext(ServerStatusContext); + const { serverConfig, setFieldInConfigState } = serverStatusData || {}; + const { notifications } = serverConfig || {}; + const { browser } = notifications || {}; + + const { enabled, goLiveMessage } = browser || {}; + + const [formDataValues, setFormDataValues] = useState({}); + const [submitStatus, setSubmitStatus] = useState(null); + + const [enableSaveButton, setEnableSaveButton] = useState(false); + + useEffect(() => { + setFormDataValues({ + enabled, + goLiveMessage, + }); + }, [notifications, browser]); + + const canSave = (): boolean => true; + + // update individual values in state + const handleFieldChange = ({ fieldName, value }: UpdateArgs) => { + console.log(fieldName, value); + setFormDataValues({ + ...formDataValues, + [fieldName]: value, + }); + + setEnableSaveButton(canSave()); + }; + + // toggle switch. + const handleSwitchChange = (switchEnabled: boolean) => { + // setShouldDisplayForm(storageEnabled); + handleFieldChange({ fieldName: 'enabled', value: switchEnabled }); + }; + + let resetTimer = null; + const resetStates = () => { + setSubmitStatus(null); + resetTimer = null; + clearTimeout(resetTimer); + }; + + const save = async () => { + const postValue = formDataValues; + + await postConfigUpdateToAPI({ + apiPath: '/notifications/browser', + data: { value: postValue }, + onSuccess: () => { + setFieldInConfigState({ + fieldName: 'browser', + value: postValue, + path: 'notifications', + }); + setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.')); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + onError: (message: string) => { + setSubmitStatus(createInputStatus(STATUS_ERROR, message)); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + }); + }; + + return ( + <> + Browser Alerts +

+ Viewers can opt into being notified when you go live with their browser. +

+

Not all browsers support this.

+ +
+ +
+ + + + ); +} diff --git a/web/components/config/notification/discord.tsx b/web/components/config/notification/discord.tsx new file mode 100644 index 000000000..e62f4dd3c --- /dev/null +++ b/web/components/config/notification/discord.tsx @@ -0,0 +1,153 @@ +import { Button, Typography } from 'antd'; +import React, { useState, useContext, useEffect } from 'react'; +import { ServerStatusContext } from '../../../utils/server-status-context'; +import TextField from '../form-textfield'; +import FormStatusIndicator from '../form-status-indicator'; +import { + postConfigUpdateToAPI, + RESET_TIMEOUT, + DISCORD_CONFIG_FIELDS, +} from '../../../utils/config-constants'; +import ToggleSwitch from '../form-toggleswitch'; +import { + createInputStatus, + StatusState, + STATUS_ERROR, + STATUS_SUCCESS, +} from '../../../utils/input-statuses'; +import { UpdateArgs } from '../../../types/config-section'; + +const { Title } = Typography; + +export default function ConfigNotify() { + const serverStatusData = useContext(ServerStatusContext); + const { serverConfig, setFieldInConfigState } = serverStatusData || {}; + const { notifications } = serverConfig || {}; + const { discord } = notifications || {}; + + const { enabled, webhook, goLiveMessage } = discord || {}; + + const [formDataValues, setFormDataValues] = useState({}); + const [submitStatus, setSubmitStatus] = useState(null); + + const [enableSaveButton, setEnableSaveButton] = useState(false); + + useEffect(() => { + setFormDataValues({ + enabled, + webhook, + goLiveMessage, + }); + }, [notifications, discord]); + + const canSave = (): boolean => { + if (webhook === '' || goLiveMessage === '') { + return false; + } + + return true; + }; + + // update individual values in state + const handleFieldChange = ({ fieldName, value }: UpdateArgs) => { + setFormDataValues({ + ...formDataValues, + [fieldName]: value, + }); + + setEnableSaveButton(canSave()); + }; + + let resetTimer = null; + const resetStates = () => { + setSubmitStatus(null); + resetTimer = null; + clearTimeout(resetTimer); + }; + + const save = async () => { + const postValue = formDataValues; + + await postConfigUpdateToAPI({ + apiPath: '/notifications/discord', + data: { value: postValue }, + onSuccess: () => { + setFieldInConfigState({ + fieldName: 'discord', + value: postValue, + path: 'notifications', + }); + setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.')); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + onError: (message: string) => { + setSubmitStatus(createInputStatus(STATUS_ERROR, message)); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + }); + }; + + // toggle switch. + const handleSwitchChange = (switchEnabled: boolean) => { + // setShouldDisplayForm(storageEnabled); + handleFieldChange({ fieldName: 'enabled', value: switchEnabled }); + }; + + return ( + <> + Discord +

+ Let your Discord channel know each time you go live. +

+

+ + Create a webhook + {' '} + under Edit Channel / Integrations on your Discord channel and provide it below. +

+ + +
+ +
+
+ +
+ + + + + ); +} diff --git a/web/components/config/notification/federation.tsx b/web/components/config/notification/federation.tsx new file mode 100644 index 000000000..292c27de4 --- /dev/null +++ b/web/components/config/notification/federation.tsx @@ -0,0 +1,51 @@ +import { Button, Typography } from 'antd'; +import React, { useState, useContext, useEffect } from 'react'; +import Link from 'next/link'; +import { ServerStatusContext } from '../../../utils/server-status-context'; + +const { Title } = Typography; + +export default function ConfigNotify() { + const serverStatusData = useContext(ServerStatusContext); + const { serverConfig } = serverStatusData || {}; + const { federation } = serverConfig || {}; + + const { enabled } = federation || {}; + const [formDataValues, setFormDataValues] = useState({}); + + useEffect(() => { + setFormDataValues({ + enabled, + }); + }, [enabled]); + + return ( + <> + Fediverse Social +

+ Enabling the Fediverse social features will not just alert people to when you go live, but + also enable other functionality. +

+

+ Fediverse social features:{' '} + + {formDataValues.enabled ? 'Enabled' : 'Disabled'} + +

+ + + + + + ); +} diff --git a/web/components/config/notification/twitter.tsx b/web/components/config/notification/twitter.tsx new file mode 100644 index 000000000..309809393 --- /dev/null +++ b/web/components/config/notification/twitter.tsx @@ -0,0 +1,198 @@ +import { Button, Typography } from 'antd'; +import React, { useState, useContext, useEffect } from 'react'; +import { ServerStatusContext } from '../../../utils/server-status-context'; +import TextField, { TEXTFIELD_TYPE_PASSWORD } from '../form-textfield'; +import FormStatusIndicator from '../form-status-indicator'; +import { + postConfigUpdateToAPI, + RESET_TIMEOUT, + TWITTER_CONFIG_FIELDS, +} from '../../../utils/config-constants'; +import ToggleSwitch from '../form-toggleswitch'; +import { + createInputStatus, + StatusState, + STATUS_ERROR, + STATUS_SUCCESS, +} from '../../../utils/input-statuses'; +import { UpdateArgs } from '../../../types/config-section'; +import { TEXTFIELD_TYPE_TEXT } from '../form-textfield-with-submit'; + +const { Title } = Typography; + +export default function ConfigNotify() { + const serverStatusData = useContext(ServerStatusContext); + const { serverConfig, setFieldInConfigState } = serverStatusData || {}; + const { notifications } = serverConfig || {}; + const { twitter } = notifications || {}; + + const { enabled, apiKey, apiSecret, accessToken, accessTokenSecret, bearerToken, goLiveMessage } = + twitter || {}; + + const [formDataValues, setFormDataValues] = useState({}); + const [submitStatus, setSubmitStatus] = useState(null); + + const [enableSaveButton, setEnableSaveButton] = useState(false); + + useEffect(() => { + setFormDataValues({ + enabled, + apiKey, + apiSecret, + accessToken, + accessTokenSecret, + bearerToken, + goLiveMessage, + }); + }, [twitter]); + + const canSave = (): boolean => true; + + // update individual values in state + const handleFieldChange = ({ fieldName, value }: UpdateArgs) => { + setFormDataValues({ + ...formDataValues, + [fieldName]: value, + }); + + setEnableSaveButton(canSave()); + }; + + // toggle switch. + const handleSwitchChange = (switchEnabled: boolean) => { + handleFieldChange({ fieldName: 'enabled', value: switchEnabled }); + }; + + let resetTimer = null; + const resetStates = () => { + setSubmitStatus(null); + resetTimer = null; + clearTimeout(resetTimer); + }; + + const save = async () => { + const postValue = formDataValues; + + await postConfigUpdateToAPI({ + apiPath: '/notifications/twitter', + data: { value: postValue }, + onSuccess: () => { + setFieldInConfigState({ + fieldName: 'twitter', + value: postValue, + path: 'notifications', + }); + setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.')); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + onError: (message: string) => { + setSubmitStatus(createInputStatus(STATUS_ERROR, message)); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + }); + }; + + return ( + <> + Twitter +

+ Let your Twitter followers know each time you go live. +

+
+

+ + Read how to configure your Twitter account + {' '} + to support posting from Owncast. +

+

+ + And then get your Twitter developer credentials + {' '} + to fill in below. +

+
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + + + ); +} diff --git a/web/components/main-layout.tsx b/web/components/main-layout.tsx index cef189b45..73ecf463c 100644 --- a/web/components/main-layout.tsx +++ b/web/components/main-layout.tsx @@ -196,6 +196,9 @@ export default function MainLayout(props) { Social + + Notifications + S3 Storage diff --git a/web/pages/config-notify.tsx b/web/pages/config-notify.tsx new file mode 100644 index 000000000..9bab47902 --- /dev/null +++ b/web/pages/config-notify.tsx @@ -0,0 +1,135 @@ +import { Alert, Button, Col, Row, Typography } from 'antd'; +import React, { useContext, useEffect, useState } from 'react'; +import Link from 'next/link'; + +import Discord from '../components/config/notification/discord'; +import Browser from '../components/config/notification/browser'; +import Twitter from '../components/config/notification/twitter'; +import Federation from '../components/config/notification/federation'; +import TextFieldWithSubmit, { + TEXTFIELD_TYPE_URL, +} from '../components/config/form-textfield-with-submit'; +import { TEXTFIELD_PROPS_FEDERATION_INSTANCE_URL } from '../utils/config-constants'; +import { ServerStatusContext } from '../utils/server-status-context'; +import { UpdateArgs } from '../types/config-section'; + +const { Title } = Typography; + +export default function ConfigNotify() { + const [formDataValues, setFormDataValues] = useState(null); + const serverStatusData = useContext(ServerStatusContext); + const { serverConfig } = serverStatusData || {}; + const { yp } = serverConfig; + const { instanceUrl } = yp; + + useEffect(() => { + setFormDataValues({ + instanceUrl, + }); + }, [yp]); + + const handleSubmitInstanceUrl = () => { + setFormDataValues({ + ...formDataValues, + enabled: false, + }); + }; + + const handleFieldChange = ({ fieldName, value }: UpdateArgs) => { + setFormDataValues({ + ...formDataValues, + [fieldName]: value, + }); + }; + + const enabled = instanceUrl !== ''; + console.log(enabled); + const configurationWarning = !enabled && ( + <> + +
+ + + ); + + return ( + <> + Notifications +

+ Let your viewers know when you go live by supporting some of the following notification + channels. +

+ + {configurationWarning} + + + + + + + + + + + + + + + + + + + Custom +

Build your own notifications by using custom webhooks.

+ + + + + +
+ + ); +} diff --git a/web/styles/globals.scss b/web/styles/globals.scss index 49d63e870..f2f0d44ba 100644 --- a/web/styles/globals.scss +++ b/web/styles/globals.scss @@ -36,6 +36,12 @@ p.description, margin: 1em 0; color: var(--white-75); } + +.description.reduced-margins { + margin-top: 5px; + margin-bottom: 5px; +} + pre { display: block; padding: 1rem; @@ -86,6 +92,11 @@ strong { } } +.form-module.disabled { + opacity: 0.4; + cursor: not-allowed; +} + .row { display: flex; flex-direction: row; diff --git a/web/types/config-section.ts b/web/types/config-section.ts index 87a0f2199..65df5b9bb 100644 --- a/web/types/config-section.ts +++ b/web/types/config-section.ts @@ -98,6 +98,33 @@ export interface Federation { blockedDomains: string[]; } +export interface BrowserNotification { + enabled: boolean; + goLiveMessage: string; +} + +export interface DiscordNotification { + enabled: boolean; + webhook: string; + goLiveMessage: string; +} + +export interface TwitterNotification { + enabled: boolean; + apiKey: string; + apiSecret: string; + accessToken: string; + accessTokenSecret: string; + bearerToken: string; + goLiveMessage: string; +} + +export interface NotificationsConfig { + browser: BrowserNotification; + discord: DiscordNotification; + twitter: TwitterNotification; +} + export interface ConfigDetails { externalActions: ExternalAction[]; ffmpegPath: string; @@ -114,7 +141,8 @@ export interface ConfigDetails { forbiddenUsernames: string[]; suggestedUsernames: string[]; chatDisabled: boolean; + federation: Federation; + notifications: NotificationsConfig; chatJoinMessagesEnabled: boolean; chatEstablishedUserMode: boolean; - federation: Federation; } diff --git a/web/utils/config-constants.tsx b/web/utils/config-constants.tsx index c9f4c4d07..24cc021c7 100644 --- a/web/utils/config-constants.tsx +++ b/web/utils/config-constants.tsx @@ -489,3 +489,78 @@ export const S3_TEXT_FIELDS_INFO = { tip: "If your S3 provider doesn't support virtual-hosted-style URLs set this to ON (i.e. Oracle Cloud Object Storage)", }, }; + +export const DISCORD_CONFIG_FIELDS = { + webhookUrl: { + fieldName: 'webhook', + label: 'Webhook URL', + maxLength: 255, + placeholder: 'https://discord.com/api/webhooks/837/jf38-6iNEv', + tip: 'The webhook assigned to your channel.', + type: TEXTFIELD_TYPE_URL, + pattern: DEFAULT_TEXTFIELD_URL_PATTERN, + useTrim: true, + }, + goLiveMessage: { + fieldName: 'goLiveMessage', + label: 'Go Live Text', + maxLength: 300, + tip: 'The text to send when you go live.', + placeholder: `I've gone live! Come watch!`, + }, +}; + +export const BROWSER_PUSH_CONFIG_FIELDS = { + goLiveMessage: { + fieldName: 'goLiveMessage', + label: 'Go Live Text', + maxLength: 200, + tip: 'The text to send when you go live.', + placeholder: `I've gone live! Come watch!`, + }, +}; + +export const TWITTER_CONFIG_FIELDS = { + apiKey: { + fieldName: 'apiKey', + label: 'API Key', + maxLength: 200, + tip: '', + placeholder: `gaUQhRC2lqfrEFfElBXJgOctU`, + }, + apiSecret: { + fieldName: 'apiSecret', + label: 'API Secret', + maxLength: 200, + tip: '', + placeholder: `IIz4jFZMWbUKdFOEGUprFjRwIslG56d1SPQlolJYjXwJ2y2qKS`, + }, + accessToken: { + fieldName: 'accessToken', + label: 'Access Token', + maxLength: 200, + tip: '', + placeholder: `952540400-EEiwe9fkuSvWjnNC82YFa9kgpqbyAP3J7FjE2dkka`, + }, + accessTokenSecret: { + fieldName: 'accessTokenSecret', + label: 'Access Token Secret', + maxLength: 200, + tip: '', + placeholder: `xO0AZWNGfZxpNsYPg3zNEKhAsPPGvNZFlzQArA2khI9Kg`, + }, + bearerToken: { + fieldName: 'bearerToken', + label: 'Bearer Token', + maxLength: 200, + tip: '', + placeholder: `AAAAAAAAAAAAAAFqpXwEAAnnepHkjA8XD5ftx5jUadYIRtPtaq7AAAAwpXPpDWKDcdhiWr0tVDjsgW%2B4awGOM9VQ%3XPoMFuWcHsE42TK`, + }, + goLiveMessage: { + fieldName: 'goLiveMessage', + label: 'Go Live Text', + maxLength: 200, + tip: 'The text to send when you go live.', + placeholder: `I've gone live! Come watch!`, + }, +}; diff --git a/web/utils/server-status-context.tsx b/web/utils/server-status-context.tsx index 81d580fe9..3ae66f36f 100644 --- a/web/utils/server-status-context.tsx +++ b/web/utils/server-status-context.tsx @@ -54,6 +54,19 @@ export const initialServerConfigState: ConfigDetails = { showEngagement: true, blockedDomains: [], }, + notifications: { + browser: { enabled: false, goLiveMessage: '' }, + discord: { enabled: false, webhook: '', goLiveMessage: '' }, + twitter: { + enabled: false, + goLiveMessage: '', + apiKey: '', + apiSecret: '', + accessToken: '', + accessTokenSecret: '', + bearerToken: '', + }, + }, externalActions: [], supportedCodecs: [], videoCodec: '',