Merge branch 'webv2' into fix/ImplementPasswordRules

This commit is contained in:
Jambaldorj Ochirpurev
2023-01-29 11:31:36 +01:00
committed by GitHub
519 changed files with 6047 additions and 3281 deletions

View File

@@ -0,0 +1,118 @@
import React, { useState, useEffect, useContext, FC } from 'react';
import { Typography, Button } from 'antd';
import CodeMirror from '@uiw/react-codemirror';
import { bbedit } from '@uiw/codemirror-theme-bbedit';
import { javascript } from '@codemirror/lang-javascript';
import { ServerStatusContext } from '../../utils/server-status-context';
import {
postConfigUpdateToAPI,
RESET_TIMEOUT,
API_CUSTOM_JAVASCRIPT,
} from '../../utils/config-constants';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import { FormStatusIndicator } from './FormStatusIndicator';
const { Title } = Typography;
// eslint-disable-next-line import/prefer-default-export
export const EditCustomJavascript: FC = () => {
const [content, setContent] = useState('/* Enter custom Javascript here */');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [hasChanged, setHasChanged] = useState(false);
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { instanceDetails } = serverConfig;
const { customJavascript: initialContent } = instanceDetails;
let resetTimer = null;
// Clear out any validation states and messaging
const resetStates = () => {
setSubmitStatus(null);
setHasChanged(false);
clearTimeout(resetTimer);
resetTimer = null;
};
// posts all the tags at once as an array obj
async function handleSave() {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({
apiPath: API_CUSTOM_JAVASCRIPT,
data: { value: content },
onSuccess: (message: string) => {
setFieldInConfigState({
fieldName: 'customJavascript',
value: content,
path: 'instanceDetails',
});
setSubmitStatus(createInputStatus(STATUS_SUCCESS, message));
},
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
},
});
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}
useEffect(() => {
setContent(initialContent);
}, [instanceDetails]);
const onCSSValueChange = React.useCallback(value => {
setContent(value);
if (value !== initialContent && !hasChanged) {
setHasChanged(true);
} else if (value === initialContent && hasChanged) {
setHasChanged(false);
}
}, []);
return (
<div className="edit-custom-css">
<Title level={3} className="section-title">
Customize your page styling with CSS
</Title>
<p className="description">
Customize the look and feel of your Owncast instance by overriding the CSS styles of various
components on the page. Refer to the{' '}
<a href="https://owncast.online/docs/website/" rel="noopener noreferrer" target="_blank">
CSS &amp; Components guide
</a>
.
</p>
<p className="description">
Please input plain CSS text, as this will be directly injected onto your page during load.
</p>
<CodeMirror
value={content}
placeholder="/* Enter custom Javascript here */"
theme={bbedit}
height="200px"
extensions={[javascript()]}
onChange={onCSSValueChange}
/>
<br />
<div className="page-content-actions">
{hasChanged && (
<Button type="primary" onClick={handleSave}>
Save
</Button>
)}
<FormStatusIndicator status={submitStatus} />
</div>
</div>
);
};

View File

@@ -261,7 +261,7 @@ export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
},
upgradeVersion && {
key: 'upgrade',
label: <Link href="/upgrade">{upgradeMessage}</Link>,
label: <Link href="/admin/upgrade">{upgradeMessage}</Link>,
},
{
key: 'help',

View File

@@ -125,7 +125,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
if (!config?.federation?.enabled) {
data.push({
icon: <img alt="fediverse" width="20px" src="fediverse-white.png" />,
icon: <img alt="fediverse" width="20px" src="/img/fediverse-color.png" />,
title: 'Add your Owncast instance to the Fediverse',
content: (
<div>

View File

@@ -284,6 +284,8 @@ export const VideoVariantForm: FC<VideoVariantFormProps> = ({
onConfirm={handleVideoPassConfirm}
okText="Yes"
cancelText="No"
getPopupContainer={triggerNode => triggerNode}
placement="topLeft"
>
{/* adding an <a> tag to force Popcofirm to register click on toggle */}
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}

View File

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { FC, useContext, useEffect, useState } from 'react';
import { Button, Col, Collapse, Row, Slider, Space } from 'antd';
import Paragraph from 'antd/lib/typography/Paragraph';
@@ -35,49 +35,39 @@ const chatColorVariables = [
{ name: 'theme-color-users-7', description: '' },
];
const paletteVariables = [
{ name: 'theme-color-palette-0', description: '' },
{ name: 'theme-color-palette-1', description: '' },
{ name: 'theme-color-palette-2', description: '' },
{ name: 'theme-color-palette-3', description: '' },
{ name: 'theme-color-palette-4', description: '' },
{ name: 'theme-color-palette-5', description: '' },
{ name: 'theme-color-palette-6', description: '' },
{ name: 'theme-color-palette-7', description: '' },
{ name: 'theme-color-palette-8', description: '' },
{ name: 'theme-color-palette-9', description: '' },
{ name: 'theme-color-palette-10', description: '' },
{ name: 'theme-color-palette-11', description: '' },
{ name: 'theme-color-palette-12', description: '' },
];
const componentColorVariables = [
{ name: 'theme-color-background-main', description: 'Background' },
{ name: 'theme-color-action', description: 'Action' },
{ name: 'theme-color-action-hover', description: 'Action Hover' },
{ name: 'theme-color-components-primary-button-border', description: 'Primary Button Border' },
{ name: 'theme-color-components-primary-button-text', description: 'Primary Button Text' },
{ name: 'theme-color-components-chat-background', description: 'Chat Background' },
{ name: 'theme-color-components-chat-text', description: 'Text: Chat' },
{ name: 'theme-color-components-text-on-dark', description: 'Text: Light' },
{ name: 'theme-color-components-text-on-light', description: 'Text: Dark' },
{ name: 'theme-color-background-header', description: 'Header/Footer' },
{ name: 'theme-color-components-content-background', description: 'Page Content' },
{ name: 'theme-color-components-scrollbar-background', description: 'Scrollbar Background' },
{ name: 'theme-color-components-scrollbar-thumb', description: 'Scrollbar Thumb' },
{
name: 'theme-color-components-video-status-bar-background',
description: 'Video Status Bar Background',
},
{
name: 'theme-color-components-video-status-bar-foreground',
description: 'Video Status Bar Foreground',
},
];
const others = [{ name: 'theme-rounded-corners', description: 'Corner radius' }];
// Create an object so these vars can be indexed by name.
const allAvailableValues = [
...paletteVariables,
...componentColorVariables,
...chatColorVariables,
...others,
].reduce((obj, val) => {
// eslint-disable-next-line no-param-reassign
obj[val.name] = { name: val.name, description: val.description };
return obj;
}, {});
const allAvailableValues = [...componentColorVariables, ...chatColorVariables, ...others].reduce(
(obj, val) => {
// eslint-disable-next-line no-param-reassign
obj[val.name] = { name: val.name, description: val.description };
return obj;
},
{},
);
// eslint-disable-next-line react/function-component-definition
function ColorPicker({
@@ -106,6 +96,7 @@ function ColorPicker({
</Col>
);
}
// eslint-disable-next-line react/function-component-definition
export default function Appearance() {
const serverStatusData = useContext(ServerStatusContext);
@@ -113,7 +104,9 @@ export default function Appearance() {
const { instanceDetails } = serverConfig;
const { appearanceVariables } = instanceDetails;
const [colors, setColors] = useState<Record<string, AppearanceVariable>>();
const [defaultValues, setDefaultValues] = useState<Record<string, AppearanceVariable>>();
const [customValues, setCustomValues] = useState<Record<string, AppearanceVariable>>();
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
let resetTimer = null;
@@ -123,39 +116,37 @@ export default function Appearance() {
clearTimeout(resetTimer);
};
const setColorDefaults = () => {
const setDefaults = () => {
const c = {};
[...paletteVariables, ...componentColorVariables, ...chatColorVariables, ...others].forEach(
color => {
const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(
`--${color.name}`,
);
c[color.name] = { value: resolvedColor.trim(), description: color.description };
},
);
setColors(c);
[...componentColorVariables, ...chatColorVariables, ...others].forEach(color => {
const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(
`--${color.name}`,
);
c[color.name] = { value: resolvedColor.trim(), description: color.description };
});
setDefaultValues(c);
};
useEffect(() => {
setColorDefaults();
setDefaults();
}, []);
useEffect(() => {
if (Object.keys(appearanceVariables).length === 0) return;
const c = colors || {};
const c = {};
Object.keys(appearanceVariables).forEach(key => {
c[key] = {
value: appearanceVariables[key],
description: allAvailableValues[key]?.description || '',
};
});
setColors(c);
setCustomValues(c);
}, [appearanceVariables]);
const updateColor = (variable: string, color: string, description: string) => {
setColors({
...colors,
setCustomValues({
...customValues,
[variable]: { value: color, description },
});
};
@@ -167,7 +158,7 @@ export default function Appearance() {
onSuccess: () => {
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
setColorDefaults();
setCustomValues(null);
},
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
@@ -178,8 +169,8 @@ export default function Appearance() {
const save = async () => {
const c = {};
Object.keys(colors).forEach(color => {
c[color] = colors[color].value;
Object.keys(customValues).forEach(color => {
c[color] = customValues[color].value;
});
await postConfigUpdateToAPI({
@@ -202,7 +193,31 @@ export default function Appearance() {
updateColor(variableName, `${value.toString()}px`, '');
};
if (!colors) {
type ColorCollectionProps = {
variables: { name; description }[];
};
// eslint-disable-next-line react/no-unstable-nested-components
const ColorCollection: FC<ColorCollectionProps> = ({ variables }) => {
const cc = variables.map(colorVar => {
const source = customValues?.[colorVar.name] ? customValues : defaultValues;
const { name, description } = colorVar;
const { value } = source[name];
return (
<ColorPicker
key={name}
value={value}
name={name}
description={description}
onChange={updateColor}
/>
);
});
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{cc}</>;
};
if (!defaultValues) {
return <div>Loading...</div>;
}
@@ -217,56 +232,15 @@ export default function Appearance() {
Certain sections of the interface can be customized by selecting new colors for them.
</p>
<Row gutter={[16, 16]}>
{componentColorVariables.map(colorVar => {
const { name } = colorVar;
const c = colors[name];
return (
<ColorPicker
key={name}
value={c.value}
name={name}
description={c.description}
onChange={updateColor}
/>
);
})}
<ColorCollection variables={componentColorVariables} />
</Row>
</Panel>
<Panel header={<Title level={3}>Chat User Colors</Title>} key="2">
<Row gutter={[16, 16]}>
{chatColorVariables.map(colorVar => {
const { name } = colorVar;
const c = colors[name];
return (
<ColorPicker
key={name}
value={c.value}
name={name}
description={c.description}
onChange={updateColor}
/>
);
})}
</Row>
</Panel>
<Panel header={<Title level={3}>Theme Colors</Title>} key="3">
<Row gutter={[16, 16]}>
{paletteVariables.map(colorVar => {
const { name } = colorVar;
const c = colors[name];
return (
<ColorPicker
key={name}
value={c.value}
name={name}
description={c.description}
onChange={updateColor}
/>
);
})}
<ColorCollection variables={chatColorVariables} />
</Row>
</Panel>
<Panel header={<Title level={3}>Other Settings</Title>} key="4">
How rounded should corners be?
<Row gutter={[16, 16]}>
@@ -278,7 +252,9 @@ export default function Appearance() {
onChange={v => {
onBorderRadiusChange(v);
}}
value={Number(colors['theme-rounded-corners']?.value?.replace('px', '') || 0)}
value={Number(
defaultValues['theme-rounded-corners']?.value?.replace('px', '') || 0,
)}
/>
</Col>
<Col span={4}>
@@ -286,7 +262,7 @@ export default function Appearance() {
style={{
width: '100px',
height: '30px',
borderRadius: `${colors['theme-rounded-corners']?.value}`,
borderRadius: `${defaultValues['theme-rounded-corners']?.value}`,
backgroundColor: 'var(--theme-color-palette-7)',
}}
/>

View File

@@ -1,220 +0,0 @@
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 '../TextField';
import { FormStatusIndicator } from '../FormStatusIndicator';
import {
postConfigUpdateToAPI,
RESET_TIMEOUT,
TWITTER_CONFIG_FIELDS,
} from '../../../utils/config-constants';
import { ToggleSwitch } from '../ToggleSwitch';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_SUCCESS,
} from '../../../utils/input-statuses';
import { UpdateArgs } from '../../../types/config-section';
import { TEXTFIELD_TYPE_TEXT } from '../TextFieldWithSubmit';
const { Title } = Typography;
export const ConfigNotify = () => {
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { notifications } = serverConfig || {};
const { twitter } = notifications || {};
const [formDataValues, setFormDataValues] = useState<any>({});
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [enableSaveButton, setEnableSaveButton] = useState<boolean>(false);
useEffect(() => {
const {
enabled,
apiKey,
apiSecret,
accessToken,
accessTokenSecret,
bearerToken,
goLiveMessage,
} = twitter || {};
setFormDataValues({
enabled,
apiKey,
apiSecret,
accessToken,
accessTokenSecret,
bearerToken,
goLiveMessage,
});
}, [twitter]);
const canSave = (): boolean => {
const { apiKey, apiSecret, accessToken, accessTokenSecret, bearerToken, goLiveMessage } =
formDataValues;
return (
!!apiKey &&
!!apiSecret &&
!!accessToken &&
!!accessTokenSecret &&
!!bearerToken &&
!!goLiveMessage
);
};
useEffect(() => {
setEnableSaveButton(canSave());
}, [formDataValues]);
// update individual values in state
const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
setFormDataValues({
...formDataValues,
[fieldName]: value,
});
};
// toggle switch.
const handleSwitchChange = (switchEnabled: boolean) => {
const previouslySaved = formDataValues.enabled;
handleFieldChange({ fieldName: 'enabled', value: switchEnabled });
return switchEnabled !== previouslySaved;
};
let resetTimer = null;
const resetStates = () => {
setSubmitStatus(null);
resetTimer = null;
clearTimeout(resetTimer);
setEnableSaveButton(false);
};
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 (
<>
<Title>Twitter</Title>
<p className="description reduced-margins">
Let your Twitter followers know each time you go live.
</p>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<p className="description reduced-margins">
<a href="https://owncast.online/docs/notifications" target="_blank" rel="noreferrer">
Read how to configure your Twitter account
</a>{' '}
to support posting from Owncast.
</p>
<p className="description reduced-margins">
<a
href="https://developer.twitter.com/en/portal/dashboard"
target="_blank"
rel="noreferrer"
>
And then get your Twitter developer credentials
</a>{' '}
to fill in below.
</p>
</div>
<ToggleSwitch
apiPath=""
fieldName="enabled"
label="Enable Twitter"
onChange={handleSwitchChange}
checked={formDataValues.enabled}
/>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.apiKey}
required
value={formDataValues.apiKey}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.apiSecret}
type={TEXTFIELD_TYPE_PASSWORD}
required
value={formDataValues.apiSecret}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.accessToken}
required
value={formDataValues.accessToken}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.accessTokenSecret}
type={TEXTFIELD_TYPE_PASSWORD}
required
value={formDataValues.accessTokenSecret}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.bearerToken}
required
value={formDataValues.bearerToken}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.goLiveMessage}
type={TEXTFIELD_TYPE_TEXT}
required
value={formDataValues.goLiveMessage}
onChange={handleFieldChange}
/>
</div>
<Button
type="primary"
onClick={save}
style={{
display: enableSaveButton ? 'inline-block' : 'none',
position: 'relative',
marginLeft: 'auto',
right: '0',
marginTop: '20px',
}}
>
Save
</Button>
<FormStatusIndicator status={submitStatus} />
</>
);
};
export default ConfigNotify;