reafctor: normalize component formatting (#2082)

* refactor: move/rename BanUserButton file

* refactor: move/rename Chart file

* refactor: update generic component filenames to PascalCase

* refactor: update config component filenames to PascalCase

* refactor: update AdminLayout component filename to PascalCase

* refactor: update/move VideoJS component

* chore(eslint): disable bad react/require-default-props rule

* refactor: normalize ActionButton component

* refactor: normalize ActionButtonRow component

* refactor: normalize FollowButton component

* refactor: normalize NotifyButton component

* refactor: normalize ChatActionMessage component

* refactor: normalize ChatContainer component

* refactor: normalize ChatJoinMessage component

* refactor: normalize ChatModerationActionMenu component

* refactor: normalize ChatModerationDetailsModal component

* refactor: normalize ChatModeratorNotification component

* refactor: normalize ChatSocialMessage component

* refactor: normalize ChatSystemMessage component

* refactor: normalize ChatTextField component

* refactor: normalize ChatUserBadge component

* refactor: normalize ChatUserMessage component

* refactor: normalize ContentHeader component

* refactor: normalize OwncastLogo component

* refactor: normalize UserDropdown component

* chore(eslint): modify react/function-component-definition rule

* refactor: normalize CodecSelector component

* refactor: update a bunch of functional components using eslint

* refactor: update a bunch of functional components using eslint, pt2

* refactor: update a bunch of functional components using eslint, pt3

* refactor: replace all component->component default imports with named imports

* refactor: replace all component-stories->component default imports with named imports

* refactor: remove default exports from most components

* chore(eslint): add eslint config files for the components and pages dirs

* fix: use-before-define error in ChatContainer

* Fix ChatContainer import

* Only process .tsx files in Next builds

Co-authored-by: Gabe Kangas <gabek@real-ity.com>
This commit is contained in:
James Young 2022-09-07 09:00:28 +02:00 committed by GitHub
parent ee333ef10a
commit d1f3fffe2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
178 changed files with 1258 additions and 1227 deletions

View file

@ -22,7 +22,9 @@ module.exports = {
ignorePatterns: ['!./storybook/**'], ignorePatterns: ['!./storybook/**'],
rules: { rules: {
'prettier/prettier': 'error', 'prettier/prettier': 'error',
'react/prop-types': 0,
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'react/require-default-props': 'off',
'react/jsx-filename-extension': [ 'react/jsx-filename-extension': [
1, 1,
{ {
@ -31,7 +33,13 @@ module.exports = {
], ],
'react/jsx-props-no-spreading': 'off', 'react/jsx-props-no-spreading': 'off',
'react/jsx-no-bind': 'off', 'react/jsx-no-bind': 'off',
'react/function-component-definition': 'off', 'react/function-component-definition': [
'warn',
{
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
},
],
'@next/next/no-img-element': 'off', '@next/next/no-img-element': 'off',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error', '@typescript-eslint/no-unused-vars': 'error',

View file

@ -1,6 +1,5 @@
import { Meta } from '@storybook/addon-docs'; import { Meta } from '@storybook/addon-docs';
import { Typography } from 'antd'; import { Typography } from 'antd';
import UserChatMessage from '../../components/chat/ChatUserMessage';
import { ChatMessage } from '../../interfaces/chat-message.model'; import { ChatMessage } from '../../interfaces/chat-message.model';
<Meta title="Owncast/Documentation/Chat" /> <Meta title="Owncast/Documentation/Chat" />

View file

@ -0,0 +1,22 @@
// ESLint rules specific to writing react components.
module.exports = {
rules: {
// Prefer arrow functions when defining functional components
// This enables the `export const Foo: FC<FooProps> = ({ bar }) => ...` style we prefer.
'react/function-component-definition': [
'warn',
{
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
},
],
// In functional components, mostly ensures Props are defined above components.
'@typescript-eslint/no-use-before-define': 'error',
// React components tend to use named exports.
// Additionally, the `export default function` syntax cannot be auto-fixed by eslint when using ts.
'import/prefer-default-export': 'off',
},
};

View file

@ -1,15 +1,17 @@
import { Modal, Button } from 'antd'; import { Modal, Button } from 'antd';
import { ExclamationCircleFilled, QuestionCircleFilled, StopTwoTone } from '@ant-design/icons'; import { ExclamationCircleFilled, QuestionCircleFilled, StopTwoTone } from '@ant-design/icons';
import { USER_ENABLED_TOGGLE, fetchData } from '../../../utils/apis'; import { FC } from 'react';
import { User } from '../../../types/chat'; import { USER_ENABLED_TOGGLE, fetchData } from '../utils/apis';
import { User } from '../types/chat';
interface BanUserButtonProps { export type BanUserButtonProps = {
user: User; user: User;
isEnabled: Boolean; // = this user's current status isEnabled: Boolean; // = this user's current status
label?: string; label?: string;
onClick?: () => void; onClick?: () => void;
} };
export default function BanUserButton({ user, isEnabled, label, onClick }: BanUserButtonProps) {
export const BanUserButton: FC<BanUserButtonProps> = ({ user, isEnabled, label, onClick }) => {
async function buttonClicked({ id }): Promise<Boolean> { async function buttonClicked({ id }): Promise<Boolean> {
const data = { const data = {
userId: id, userId: id,
@ -78,7 +80,7 @@ export default function BanUserButton({ user, isEnabled, label, onClick }: BanUs
{label || actionString} {label || actionString}
</Button> </Button>
); );
} };
BanUserButton.defaultProps = { BanUserButton.defaultProps = {
label: '', label: '',
onClick: null, onClick: null,

View file

@ -1,7 +1,7 @@
import { Table, Button } from 'antd'; import { Table, Button } from 'antd';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { SortOrder } from 'antd/lib/table/interface'; import { SortOrder } from 'antd/lib/table/interface';
import React from 'react'; import React, { FC } from 'react';
import { StopTwoTone } from '@ant-design/icons'; import { StopTwoTone } from '@ant-design/icons';
import { User } from '../types/chat'; import { User } from '../types/chat';
import { BANNED_IP_REMOVE, fetchData } from '../utils/apis'; import { BANNED_IP_REMOVE, fetchData } from '../utils/apis';
@ -23,7 +23,11 @@ async function removeIPAddressBan(ipAddress: String) {
} }
} }
export default function BannedIPsTable({ data }: UserTableProps) { export type UserTableProps = {
data: User[];
};
export const BannedIPsTable: FC<UserTableProps> = ({ data }) => {
const columns = [ const columns = [
{ {
title: 'IP Address', title: 'IP Address',
@ -68,8 +72,4 @@ export default function BannedIPsTable({ data }: UserTableProps) {
rowKey="ipAddress" rowKey="ipAddress"
/> />
); );
} };
interface UserTableProps {
data: User[];
}

View file

@ -2,6 +2,7 @@ import ChartJs from 'chart.js/auto';
import Chartkick from 'chartkick'; import Chartkick from 'chartkick';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { LineChart } from 'react-chartkick'; import { LineChart } from 'react-chartkick';
import { FC } from 'react';
// from https://github.com/ankane/chartkick.js/blob/master/chart.js/chart.esm.js // from https://github.com/ankane/chartkick.js/blob/master/chart.js/chart.esm.js
Chartkick.use(ChartJs); Chartkick.use(ChartJs);
@ -11,7 +12,7 @@ interface TimedValue {
value: number; value: number;
} }
interface ChartProps { export type ChartProps = {
data?: TimedValue[]; data?: TimedValue[];
title?: string; title?: string;
color: string; color: string;
@ -19,7 +20,7 @@ interface ChartProps {
yFlipped?: boolean; yFlipped?: boolean;
yLogarithmic?: boolean; yLogarithmic?: boolean;
dataCollections?: any[]; dataCollections?: any[];
} };
function createGraphDataset(dataArray) { function createGraphDataset(dataArray) {
const dataValues = {}; const dataValues = {};
@ -31,7 +32,7 @@ function createGraphDataset(dataArray) {
return dataValues; return dataValues;
} }
export default function Chart({ export const Chart: FC<ChartProps> = ({
data, data,
title, title,
color, color,
@ -39,7 +40,7 @@ export default function Chart({
dataCollections, dataCollections,
yFlipped, yFlipped,
yLogarithmic, yLogarithmic,
}: ChartProps) { }) => {
const renderData = []; const renderData = [];
if (data && data.length > 0) { if (data && data.length > 0) {
@ -87,7 +88,7 @@ export default function Chart({
/> />
</div> </div>
); );
} };
Chart.defaultProps = { Chart.defaultProps = {
dataCollections: [], dataCollections: [],

View file

@ -3,12 +3,17 @@ import { FilterDropdownProps, SortOrder } from 'antd/lib/table/interface';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import { SearchOutlined } from '@ant-design/icons'; import { SearchOutlined } from '@ant-design/icons';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { FC } from 'react';
import { Client } from '../types/chat'; import { Client } from '../types/chat';
import UserPopover from './user-popover'; import { UserPopover } from './UserPopover';
import BanUserButton from './other/ban-user-button/ban-user-button'; import { BanUserButton } from './BanUserButton';
import { formatUAstring } from '../utils/format'; import { formatUAstring } from '../utils/format';
export default function ClientTable({ data }: ClientTableProps) { export type ClientTableProps = {
data: Client[];
};
export const ClientTable: FC<ClientTableProps> = ({ data }) => {
const columns: ColumnsType<Client> = [ const columns: ColumnsType<Client> = [
{ {
title: 'Display Name', title: 'Display Name',
@ -91,8 +96,4 @@ export default function ClientTable({ data }: ClientTableProps) {
rowKey="id" rowKey="id"
/> />
); );
} };
interface ClientTableProps {
data: Client[];
}

View file

@ -1,7 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FC } from 'react';
export function Color(props) { export type ColorProps = {
const { color } = props; color: any; // TODO specify better type
};
export const Color: FC<ColorProps> = ({ color }) => {
const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(`--${color}`); const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(`--${color}`);
const containerStyle = { const containerStyle = {
@ -48,7 +52,7 @@ export function Color(props) {
<figcaption style={colorDescriptionStyle}>{color}</figcaption> <figcaption style={colorDescriptionStyle}>{color}</figcaption>
</figure> </figure>
); );
} };
Color.propTypes = { Color.propTypes = {
color: PropTypes.string.isRequired, color: PropTypes.string.isRequired,
@ -61,7 +65,7 @@ const rowStyle = {
alignItems: 'center', alignItems: 'center',
}; };
export function ColorRow(props) { export const ColorRow = props => {
const { colors } = props; const { colors } = props;
return ( return (
@ -71,7 +75,7 @@ export function ColorRow(props) {
))} ))}
</div> </div>
); );
} };
ColorRow.propTypes = { ColorRow.propTypes = {
colors: PropTypes.arrayOf(PropTypes.string).isRequired, colors: PropTypes.arrayOf(PropTypes.string).isRequired,

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { FC, useState } from 'react';
import { Button, Space, Input, Modal } from 'antd'; import { Button, Space, Input, Modal } from 'antd';
import { STATUS_ERROR, STATUS_SUCCESS } from '../utils/input-statuses'; import { STATUS_ERROR, STATUS_SUCCESS } from '../utils/input-statuses';
@ -6,12 +6,12 @@ import { fetchData, FEDERATION_MESSAGE_SEND } from '../utils/apis';
const { TextArea } = Input; const { TextArea } = Input;
interface ComposeFederatedPostProps { export type ComposeFederatedPostProps = {
visible: boolean; visible: boolean;
handleClose: () => void; handleClose: () => void;
} };
export default function ComposeFederatedPost({ visible, handleClose }: ComposeFederatedPostProps) { export const ComposeFederatedPost: FC<ComposeFederatedPostProps> = ({ visible, handleClose }) => {
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const [postPending, setPostPending] = useState(false); const [postPending, setPostPending] = useState(false);
const [postSuccessState, setPostSuccessState] = useState(null); const [postSuccessState, setPostSuccessState] = useState(null);
@ -79,4 +79,4 @@ export default function ComposeFederatedPost({ visible, handleClose }: ComposeFe
</Space> </Space>
</Modal> </Modal>
); );
} };

View file

@ -1,6 +1,11 @@
export function ImageAsset(props: ImageAssetProps) { import { FC } from 'react';
const { name, src } = props;
export type ImageAssetProps = {
name: string;
src: string;
};
export const ImageAsset: FC<ImageAssetProps> = ({ name, src }) => {
const containerStyle = { const containerStyle = {
borderRadius: '20px', borderRadius: '20px',
width: '12vw', width: '12vw',
@ -38,12 +43,7 @@ export function ImageAsset(props: ImageAssetProps) {
</a> </a>
</figure> </figure>
); );
} };
interface ImageAssetProps {
name: string;
src: string;
}
const rowStyle = { const rowStyle = {
display: 'flex', display: 'flex',
@ -53,7 +53,7 @@ const rowStyle = {
alignItems: 'center', alignItems: 'center',
}; };
export function ImageRow(props: ImageRowProps) { export const ImageRow = (props: ImageRowProps) => {
const { images } = props; const { images } = props;
return ( return (
@ -63,7 +63,7 @@ export function ImageRow(props: ImageRowProps) {
))} ))}
</div> </div>
); );
} };
interface ImageRowProps { interface ImageRowProps {
images: ImageAssetProps[]; images: ImageAssetProps[];

View file

@ -1,11 +1,12 @@
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { FC } from 'react';
interface InfoTipProps { export type InfoTipProps = {
tip: string | null; tip: string | null;
} };
export default function InfoTip({ tip }: InfoTipProps) { export const InfoTip: FC<InfoTipProps> = ({ tip }) => {
if (tip === '' || tip === null) { if (tip === '' || tip === null) {
return null; return null;
} }
@ -17,4 +18,4 @@ export default function InfoTip({ tip }: InfoTipProps) {
</Tooltip> </Tooltip>
</span> </span>
); );
} };

View file

@ -1,8 +1,14 @@
import { Table, Typography } from 'antd'; import { Table, Typography } from 'antd';
import { FC } from 'react';
const { Title } = Typography; const { Title } = Typography;
export default function KeyValueTable({ title, data }: KeyValueTableProps) { export type KeyValueTableProps = {
title: string;
data: any;
};
export const KeyValueTable: FC<KeyValueTableProps> = ({ title, data }) => {
const columns = [ const columns = [
{ {
title: 'Name', title: 'Name',
@ -22,9 +28,4 @@ export default function KeyValueTable({ title, data }: KeyValueTableProps) {
<Table pagination={false} columns={columns} dataSource={data} rowKey="name" /> <Table pagination={false} columns={columns} dataSource={data} rowKey="name" />
</> </>
); );
} };
interface KeyValueTableProps {
title: string;
data: any;
}

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { FC } from 'react';
import { Table, Tag, Typography } from 'antd'; import { Table, Tag, Typography } from 'antd';
import Linkify from 'react-linkify'; import Linkify from 'react-linkify';
import { SortOrder } from 'antd/lib/table/interface'; import { SortOrder } from 'antd/lib/table/interface';
@ -22,12 +22,12 @@ function renderMessage(text) {
return <Linkify>{text}</Linkify>; return <Linkify>{text}</Linkify>;
} }
interface Props { export type LogTableProps = {
logs: object[]; logs: object[];
pageSize: number; pageSize: number;
} };
export default function LogTable({ logs, pageSize }: Props) { export const LogTable: FC<LogTableProps> = ({ logs, pageSize }) => {
if (!logs?.length) { if (!logs?.length) {
return null; return null;
} }
@ -85,4 +85,4 @@ export default function LogTable({ logs, pageSize }: Props) {
/> />
</div> </div>
); );
} };

View file

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { FC, ReactNode, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Link from 'next/link'; import Link from 'next/link';
import Head from 'next/head'; import Head from 'next/head';
@ -21,19 +21,20 @@ import classNames from 'classnames';
import { upgradeVersionAvailable } from '../utils/apis'; import { upgradeVersionAvailable } from '../utils/apis';
import { parseSecondsToDurationString } from '../utils/format'; import { parseSecondsToDurationString } from '../utils/format';
import { OwncastLogo } from './common'; import { OwncastLogo } from './common/OwncastLogo/OwncastLogo';
import { ServerStatusContext } from '../utils/server-status-context'; import { ServerStatusContext } from '../utils/server-status-context';
import { AlertMessageContext } from '../utils/alert-message-context'; import { AlertMessageContext } from '../utils/alert-message-context';
import TextFieldWithSubmit from './config/form-textfield-with-submit'; import { TextFieldWithSubmit } from './config/TextFieldWithSubmit';
import { TEXTFIELD_PROPS_STREAM_TITLE } from '../utils/config-constants'; import { TEXTFIELD_PROPS_STREAM_TITLE } from '../utils/config-constants';
import ComposeFederatedPost from './compose-federated-post'; import { ComposeFederatedPost } from './ComposeFederatedPost';
import { UpdateArgs } from '../types/config-section'; import { UpdateArgs } from '../types/config-section';
// eslint-disable-next-line react/function-component-definition export type MainLayoutProps = {
export default function MainLayout(props) { children: ReactNode;
const { children } = props; };
export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
const context = useContext(ServerStatusContext); const context = useContext(ServerStatusContext);
const { serverConfig, online, broadcaster, versionNumber } = context || {}; const { serverConfig, online, broadcaster, versionNumber } = context || {};
const { instanceDetails, chatDisabled, federation } = serverConfig; const { instanceDetails, chatDisabled, federation } = serverConfig;
@ -287,7 +288,7 @@ export default function MainLayout(props) {
/> />
</Layout> </Layout>
); );
} };
MainLayout.propTypes = { MainLayout.propTypes = {
children: PropTypes.element.isRequired, children: PropTypes.element.isRequired,

View file

@ -1,5 +1,5 @@
// Custom component for AntDesign Button that makes an api call, then displays a confirmation icon upon // Custom component for AntDesign Button that makes an api call, then displays a confirmation icon upon
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, FC } from 'react';
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
import { import {
EyeOutlined, EyeOutlined,
@ -12,17 +12,17 @@ import { MessageType } from '../types/chat';
import { OUTCOME_TIMEOUT } from '../pages/admin/chat/messages'; import { OUTCOME_TIMEOUT } from '../pages/admin/chat/messages';
import { isEmptyObject } from '../utils/format'; import { isEmptyObject } from '../utils/format';
interface MessageToggleProps { export type MessageToggleProps = {
isVisible: boolean; isVisible: boolean;
message: MessageType; message: MessageType;
setMessage: (message: MessageType) => void; setMessage: (message: MessageType) => void;
} };
export default function MessageVisiblityToggle({ export const MessageVisiblityToggle: FC<MessageToggleProps> = ({
isVisible, isVisible,
message, message,
setMessage, setMessage,
}: MessageToggleProps) { }) => {
if (!message || isEmptyObject(message)) { if (!message || isEmptyObject(message)) {
return null; return null;
} }
@ -89,4 +89,4 @@ export default function MessageVisiblityToggle({
</Tooltip> </Tooltip>
</div> </div>
); );
} };

View file

@ -5,14 +5,16 @@ import {
StopTwoTone, StopTwoTone,
SafetyCertificateTwoTone, SafetyCertificateTwoTone,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { FC } from 'react';
import { USER_SET_MODERATOR, fetchData } from '../utils/apis'; import { USER_SET_MODERATOR, fetchData } from '../utils/apis';
import { User } from '../types/chat'; import { User } from '../types/chat';
interface ModeratorUserButtonProps { export type ModeratorUserButtonProps = {
user: User; user: User;
onClick?: () => void; onClick?: () => void;
} };
export default function ModeratorUserButton({ user, onClick }: ModeratorUserButtonProps) {
export const ModeratorUserButton: FC<ModeratorUserButtonProps> = ({ user, onClick }) => {
async function buttonClicked({ id }, setAsModerator: Boolean): Promise<Boolean> { async function buttonClicked({ id }, setAsModerator: Boolean): Promise<Boolean> {
const data = { const data = {
userId: id, userId: id,
@ -87,7 +89,8 @@ export default function ModeratorUserButton({ user, onClick }: ModeratorUserButt
{actionString} {actionString}
</Button> </Button>
); );
} };
ModeratorUserButton.defaultProps = { ModeratorUserButton.defaultProps = {
onClick: null, onClick: null,
}; };

View file

@ -1,6 +1,6 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, FC } from 'react';
import { Collapse, Typography, Skeleton } from 'antd'; import { Collapse, Typography, Skeleton } from 'antd';
import format from 'date-fns/format'; import format from 'date-fns/format';
@ -12,14 +12,19 @@ const { Title, Link } = Typography;
const OWNCAST_FEED_URL = 'https://owncast.online/news/index.json'; const OWNCAST_FEED_URL = 'https://owncast.online/news/index.json';
const OWNCAST_BASE_URL = 'https://owncast.online'; const OWNCAST_BASE_URL = 'https://owncast.online';
interface Article { export type ArticleProps = {
title: string; title: string;
url: string; url: string;
content_html: string; content_html: string;
date_published: string; date_published: string;
} };
function ArticleItem({ title, url, content_html: content, date_published: date }: Article) { const ArticleItem: FC<ArticleProps> = ({
title,
url,
content_html: content,
date_published: date,
}) => {
const dateObject = new Date(date); const dateObject = new Date(date);
const dateString = format(dateObject, 'MMM dd, yyyy, HH:mm'); const dateString = format(dateObject, 'MMM dd, yyyy, HH:mm');
return ( return (
@ -38,10 +43,10 @@ function ArticleItem({ title, url, content_html: content, date_published: date }
</Collapse> </Collapse>
</article> </article>
); );
} };
export default function NewsFeed() { export const NewsFeed = () => {
const [feed, setFeed] = useState<Article[]>([]); const [feed, setFeed] = useState<ArticleProps[]>([]);
const [loading, setLoading] = useState<Boolean>(true); const [loading, setLoading] = useState<Boolean>(true);
const getFeed = async () => { const getFeed = async () => {
@ -75,4 +80,4 @@ export default function NewsFeed() {
{noNews} {noNews}
</section> </section>
); );
} };

View file

@ -1,10 +1,10 @@
import { BookTwoTone, MessageTwoTone, PlaySquareTwoTone, ProfileTwoTone } from '@ant-design/icons'; import { BookTwoTone, MessageTwoTone, PlaySquareTwoTone, ProfileTwoTone } from '@ant-design/icons';
import { Card, Col, Row, Typography } from 'antd'; import { Card, Col, Row, Typography } from 'antd';
import Link from 'next/link'; import Link from 'next/link';
import { useContext } from 'react'; import { FC, useContext } from 'react';
import LogTable from './log-table'; import { LogTable } from './LogTable';
import OwncastLogo from './common/Logo/Logo'; import { OwncastLogo } from './common/OwncastLogo/OwncastLogo';
import NewsFeed from './news-feed'; import { NewsFeed } from './NewsFeed';
import { ConfigDetails } from '../types/config-section'; import { ConfigDetails } from '../types/config-section';
import { ServerStatusContext } from '../utils/server-status-context'; import { ServerStatusContext } from '../utils/server-status-context';
@ -17,12 +17,12 @@ function generateStreamURL(serverURL, rtmpServerPort) {
return `rtmp://${serverURL.replace(/(^\w+:|^)\/\//, '')}:${rtmpServerPort}/live`; return `rtmp://${serverURL.replace(/(^\w+:|^)\/\//, '')}:${rtmpServerPort}/live`;
} }
type OfflineProps = { export type OfflineProps = {
logs: any[]; logs: any[];
config: ConfigDetails; config: ConfigDetails;
}; };
export default function Offline({ logs = [], config }: OfflineProps) { export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig } = serverStatusData || {}; const { serverConfig } = serverStatusData || {};
@ -149,4 +149,5 @@ export default function Offline({ logs = [], config }: OfflineProps) {
<LogTable logs={logs} pageSize={5} /> <LogTable logs={logs} pageSize={5} />
</> </>
); );
} };
export default Offline;

View file

@ -3,10 +3,11 @@
// TODO: This component should be cleaned up and usage should be re-examined. The types should be reconsidered as well. // TODO: This component should be cleaned up and usage should be re-examined. The types should be reconsidered as well.
import { Typography, Statistic, Card, Progress } from 'antd'; import { Typography, Statistic, Card, Progress } from 'antd';
import { FC } from 'react';
const { Text } = Typography; const { Text } = Typography;
interface StatisticItemProps { export type StatisticItemProps = {
title?: string; title?: string;
value?: any; value?: any;
prefix?: any; prefix?: any;
@ -15,7 +16,8 @@ interface StatisticItemProps {
progress?: boolean; progress?: boolean;
centered?: boolean; centered?: boolean;
formatter?: any; formatter?: any;
} };
const defaultProps = { const defaultProps = {
title: '', title: '',
value: 0, value: 0,
@ -27,31 +29,29 @@ const defaultProps = {
formatter: null, formatter: null,
}; };
interface ContentProps { export type ContentProps = {
prefix: string; prefix: string;
value: any; value: any;
suffix: string; suffix: string;
title: string; title: string;
} };
function Content({ prefix, value, suffix, title }: ContentProps) { const Content: FC<ContentProps> = ({ prefix, value, suffix, title }) => (
return ( <div>
{prefix}
<div> <div>
{prefix} <Text type="secondary">{title}</Text>
<div>
<Text type="secondary">{title}</Text>
</div>
<div>
<Text type="secondary">
{value}
{suffix || '%'}
</Text>
</div>
</div> </div>
); <div>
} <Text type="secondary">
{value}
{suffix || '%'}
</Text>
</div>
</div>
);
function ProgressView({ title, value, prefix, suffix, color }: StatisticItemProps) { const ProgressView: FC<StatisticItemProps> = ({ title, value, prefix, suffix, color }) => {
const endColor = value > 90 ? 'red' : color; const endColor = value > 90 ? 'red' : color;
const content = <Content prefix={prefix} value={value} suffix={suffix} title={title} />; const content = <Content prefix={prefix} value={value} suffix={suffix} title={title} />;
@ -67,15 +67,15 @@ function ProgressView({ title, value, prefix, suffix, color }: StatisticItemProp
format={() => content} format={() => content}
/> />
); );
} };
ProgressView.defaultProps = defaultProps; ProgressView.defaultProps = defaultProps;
function StatisticView({ title, value, prefix, formatter }: StatisticItemProps) { const StatisticView: FC<StatisticItemProps> = ({ title, value, prefix, formatter }) => (
return <Statistic title={title} value={value} prefix={prefix} formatter={formatter} />; <Statistic title={title} value={value} prefix={prefix} formatter={formatter} />
} );
StatisticView.defaultProps = defaultProps; StatisticView.defaultProps = defaultProps;
export default function StatisticItem(props: StatisticItemProps) { export const StatisticItem: FC<StatisticItemProps> = props => {
const { progress, centered } = props; const { progress, centered } = props;
const View = progress ? ProgressView : StatisticView; const View = progress ? ProgressView : StatisticView;
@ -88,5 +88,5 @@ export default function StatisticItem(props: StatisticItemProps) {
</div> </div>
</Card> </Card>
); );
} };
StatisticItem.defaultProps = defaultProps; StatisticItem.defaultProps = defaultProps;

View file

@ -1,15 +1,14 @@
import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { Alert, Button, Col, Row, Statistic, Typography } from 'antd'; import { Alert, Button, Col, Row, Statistic, Typography } from 'antd';
import Link from 'next/link'; import Link from 'next/link';
import React, { useContext } from 'react'; import React, { FC, useContext } from 'react';
import { ServerStatusContext } from '../utils/server-status-context'; import { ServerStatusContext } from '../utils/server-status-context';
interface StreamHealthOverviewProps { export type StreamHealthOverviewProps = {
showTroubleshootButton?: Boolean; showTroubleshootButton?: Boolean;
} };
export default function StreamHealthOverview({
showTroubleshootButton, export const StreamHealthOverview: FC<StreamHealthOverviewProps> = ({ showTroubleshootButton }) => {
}: StreamHealthOverviewProps) {
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { health } = serverStatusData; const { health } = serverStatusData;
if (!health) { if (!health) {
@ -79,7 +78,7 @@ export default function StreamHealthOverview({
</Row> </Row>
</div> </div>
); );
} };
StreamHealthOverview.defaultProps = { StreamHealthOverview.defaultProps = {
showTroubleshootButton: true, showTroubleshootButton: true,

View file

@ -1,25 +1,25 @@
// This displays a clickable user name (or whatever children element you provide), and displays a simple tooltip of created time. OnClick a modal with more information about the user is displayed. // This displays a clickable user name (or whatever children element you provide), and displays a simple tooltip of created time. OnClick a modal with more information about the user is displayed.
import { useState, ReactNode } from 'react'; import { useState, ReactNode, FC } from 'react';
import { Divider, Modal, Tooltip, Typography, Row, Col, Space } from 'antd'; import { Divider, Modal, Tooltip, Typography, Row, Col, Space } from 'antd';
import formatDistanceToNow from 'date-fns/formatDistanceToNow'; import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import BlockUserbutton from './other/ban-user-button/ban-user-button'; import { BanUserButton } from './BanUserButton';
import ModeratorUserbutton from './moderator-user-button'; import { ModeratorUserButton } from './ModeratorUserButton';
import { User, UserConnectionInfo } from '../types/chat'; import { User, UserConnectionInfo } from '../types/chat';
import { formatDisplayDate } from './user-table'; import { formatDisplayDate } from './UserTable';
import { formatUAstring } from '../utils/format'; import { formatUAstring } from '../utils/format';
interface UserPopoverProps { export type UserPopoverProps = {
user: User; user: User;
connectionInfo?: UserConnectionInfo | null; connectionInfo?: UserConnectionInfo | null;
children: ReactNode; children: ReactNode;
} };
export default function UserPopover({ user, connectionInfo, children }: UserPopoverProps) { export const UserPopover: FC<UserPopoverProps> = ({ user, connectionInfo, children }) => {
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const handleShowModal = () => { const handleShowModal = () => {
setIsModalVisible(true); setIsModalVisible(true);
@ -123,7 +123,7 @@ export default function UserPopover({ user, connectionInfo, children }: UserPopo
This user was banned on <code>{formatDisplayDate(disabledAt)}</code>. This user was banned on <code>{formatDisplayDate(disabledAt)}</code>.
<br /> <br />
<br /> <br />
<BlockUserbutton <BanUserButton
label="Unban this user" label="Unban this user"
user={user} user={user}
isEnabled={false} isEnabled={false}
@ -131,20 +131,20 @@ export default function UserPopover({ user, connectionInfo, children }: UserPopo
/> />
</> </>
) : ( ) : (
<BlockUserbutton <BanUserButton
label="Ban this user" label="Ban this user"
user={user} user={user}
isEnabled isEnabled
onClick={handleCloseModal} onClick={handleCloseModal}
/> />
)} )}
<ModeratorUserbutton user={user} onClick={handleCloseModal} /> <ModeratorUserButton user={user} onClick={handleCloseModal} />
</Space> </Space>
</div> </div>
</Modal> </Modal>
</> </>
); );
} };
UserPopover.defaultProps = { UserPopover.defaultProps = {
connectionInfo: null, connectionInfo: null,

View file

@ -1,14 +1,20 @@
import { Table } from 'antd'; import { Table } from 'antd';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { SortOrder } from 'antd/lib/table/interface'; import { SortOrder } from 'antd/lib/table/interface';
import { FC } from 'react';
import { User } from '../types/chat'; import { User } from '../types/chat';
import UserPopover from './user-popover'; import { UserPopover } from './UserPopover';
import BanUserButton from './other/ban-user-button/ban-user-button'; import { BanUserButton } from './BanUserButton';
export function formatDisplayDate(date: string | Date) { export function formatDisplayDate(date: string | Date) {
return format(new Date(date), 'MMM d H:mma'); return format(new Date(date), 'MMM d H:mma');
} }
export default function UserTable({ data }: UserTableProps) {
export type UserTableProps = {
data: User[];
};
export const UserTable: FC<UserTableProps> = ({ data }) => {
const columns = [ const columns = [
{ {
title: 'Last Known Display Name', title: 'Last Known Display Name',
@ -57,8 +63,4 @@ export default function UserTable({ data }: UserTableProps) {
rowKey="id" rowKey="id"
/> />
); );
} };
interface UserTableProps {
data: User[];
}

View file

@ -2,13 +2,19 @@ import { Table } from 'antd';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { SortOrder } from 'antd/lib/table/interface'; import { SortOrder } from 'antd/lib/table/interface';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { FC } from 'react';
import { User } from '../types/chat'; import { User } from '../types/chat';
import { formatUAstring } from '../utils/format'; import { formatUAstring } from '../utils/format';
export function formatDisplayDate(date: string | Date) { export function formatDisplayDate(date: string | Date) {
return format(new Date(date), 'MMM d H:mma'); return format(new Date(date), 'MMM d H:mma');
} }
export default function ViewerTable({ data }: ViewerTableProps) {
export type ViewerTableProps = {
data: User[];
};
export const ViewerTable: FC<ViewerTableProps> = ({ data }) => {
const columns = [ const columns = [
{ {
title: 'User Agent', title: 'User Agent',
@ -43,8 +49,4 @@ export default function ViewerTable({ data }: ViewerTableProps) {
rowKey="id" rowKey="id"
/> />
); );
} };
interface ViewerTableProps {
data: User[];
}

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import ActionButton from './ActionButton'; import { ActionButton } from './ActionButton';
export default { export default {
title: 'owncast/Components/Action Buttons/Single button', title: 'owncast/Components/Action Buttons/Single button',

View file

@ -1,24 +1,21 @@
import { Button } from 'antd'; import { Button } from 'antd';
import { useState } from 'react'; import { FC, useState } from 'react';
import Modal from '../../ui/Modal/Modal'; import { Modal } from '../../ui/Modal/Modal';
import { ExternalAction } from '../../../interfaces/external-action'; import { ExternalAction } from '../../../interfaces/external-action';
import s from './ActionButton.module.scss'; import styles from './ActionButton.module.scss';
interface Props { export type ActionButtonProps = {
action: ExternalAction; action: ExternalAction;
primary?: boolean; primary?: boolean;
}
ActionButton.defaultProps = {
primary: true,
}; };
export default function ActionButton({ export const ActionButton: FC<ActionButtonProps> = ({
action: { url, title, description, icon, color, openExternally }, action: { url, title, description, icon, color, openExternally },
primary = false, primary = true,
}: Props) { }) => {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const buttonClicked = () => { const onButtonClicked = () => {
if (openExternally) { if (openExternally) {
window.open(url, '_blank'); window.open(url, '_blank');
} else { } else {
@ -30,11 +27,11 @@ export default function ActionButton({
<> <>
<Button <Button
type={primary ? 'primary' : 'default'} type={primary ? 'primary' : 'default'}
className={`${s.button}`} className={`${styles.button}`}
onClick={buttonClicked} onClick={onButtonClicked}
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
> >
<img src={icon} className={`${s.icon}`} alt={description} /> <img src={icon} className={`${styles.icon}`} alt={description} />
{title} {title}
</Button> </Button>
<Modal <Modal
@ -46,4 +43,4 @@ export default function ActionButton({
/> />
</> </>
); );
} };

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import ActionButtonRow from './ActionButtonRow'; import { ActionButtonRow } from './ActionButtonRow';
import ActionButton from '../ActionButton/ActionButton'; import { ActionButton } from '../ActionButton/ActionButton';
export default { export default {
title: 'owncast/Components/Action Buttons/Buttons Row', title: 'owncast/Components/Action Buttons/Buttons Row',

View file

@ -1,12 +1,10 @@
import React from 'react'; import { FC, ReactNode } from 'react';
import s from './ActionButtonRow.module.scss'; import styles from './ActionButtonRow.module.scss';
interface Props { export type ActionButtonRowProps = {
children: React.ReactNode[]; children: ReactNode;
} };
export default function ActionButtonRow(props: Props) { export const ActionButtonRow: FC<ActionButtonRowProps> = ({ children }) => (
const { children } = props; <div className={`${styles.row}`}>{children}</div>
);
return <div className={`${s.row}`}>{children}</div>;
}

View file

@ -1,31 +1,29 @@
import { Button } from 'antd'; import { Button, ButtonProps } from 'antd';
import { HeartFilled } from '@ant-design/icons'; import { HeartFilled } from '@ant-design/icons';
import { useState } from 'react'; import { FC, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import Modal from '../ui/Modal/Modal'; import { Modal } from '../ui/Modal/Modal';
import FollowModal from '../modals/FollowModal/FollowModal'; import { FollowModal } from '../modals/FollowModal/FollowModal';
import s from './ActionButton/ActionButton.module.scss'; import styles from './ActionButton/ActionButton.module.scss';
import { clientConfigStateAtom } from '../stores/ClientConfigStore'; import { clientConfigStateAtom } from '../stores/ClientConfigStore';
import { ClientConfig } from '../../interfaces/client-config.model'; import { ClientConfig } from '../../interfaces/client-config.model';
export default function FollowButton(props: any) { export type FollowButtonProps = ButtonProps;
export const FollowButton: FC<FollowButtonProps> = props => {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom); const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const { name, federation } = clientConfig; const { name, federation } = clientConfig;
const { account } = federation; const { account } = federation;
const buttonClicked = () => {
setShowModal(true);
};
return ( return (
<> <>
<Button <Button
{...props} {...props}
type="primary" type="primary"
className={s.button} className={styles.button}
icon={<HeartFilled />} icon={<HeartFilled />}
onClick={buttonClicked} onClick={() => setShowModal(true)}
> >
Follow Follow
</Button> </Button>
@ -40,4 +38,4 @@ export default function FollowButton(props: any) {
</Modal> </Modal>
</> </>
); );
} };

View file

@ -1,15 +1,14 @@
import { Button } from 'antd'; import { Button } from 'antd';
import { BellFilled } from '@ant-design/icons'; import { BellFilled } from '@ant-design/icons';
import s from './ActionButton/ActionButton.module.scss'; import { FC } from 'react';
import styles from './ActionButton/ActionButton.module.scss';
interface Props { export type NotifyButtonProps = {
onClick: () => void; onClick?: () => void;
} };
export default function NotifyButton({ onClick }: Props) { export const NotifyButton: FC<NotifyButtonProps> = ({ onClick }) => (
return ( <Button type="primary" className={`${styles.button}`} icon={<BellFilled />} onClick={onClick}>
<Button type="primary" className={`${s.button}`} icon={<BellFilled />} onClick={onClick}> Notify
Notify </Button>
</Button> );
);
}

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import ChatActionMessage from './ChatActionMessage'; import { ChatActionMessage } from './ChatActionMessage';
import Mock from '../../../stories/assets/mocks/chatmessage-action.png'; import Mock from '../../../stories/assets/mocks/chatmessage-action.png';
export default { export default {

View file

@ -1,13 +1,12 @@
import s from './ChatActionMessage.module.scss'; import { FC } from 'react';
import styles from './ChatActionMessage.module.scss';
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
interface Props { export type ChatActionMessageProps = {
body: string; body: string;
} };
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function ChatActionMessage(props: Props) { export const ChatActionMessage: FC<ChatActionMessageProps> = ({ body }) => (
const { body } = props; <div dangerouslySetInnerHTML={{ __html: body }} className={styles.chatAction} />
);
return <div dangerouslySetInnerHTML={{ __html: body }} className={s.chatAction} />;
}

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import ChatContainer from './index'; import { ChatContainer } from './ChatContainer';
import { ChatMessage } from '../../../interfaces/chat-message.model'; import { ChatMessage } from '../../../interfaces/chat-message.model';
export default { export default {

View file

@ -1,32 +1,77 @@
import { Button } from 'antd'; import { Button } from 'antd';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
import { useState, useMemo, useRef, CSSProperties } from 'react'; import { useState, useMemo, useRef, CSSProperties, FC } from 'react';
import { EditFilled, VerticalAlignBottomOutlined } from '@ant-design/icons'; import { EditFilled, VerticalAlignBottomOutlined } from '@ant-design/icons';
import { import {
ConnectedClientInfoEvent, ConnectedClientInfoEvent,
MessageType, MessageType,
NameChangeEvent, NameChangeEvent,
} from '../../../interfaces/socket-events'; } from '../../../interfaces/socket-events';
import s from './ChatContainer.module.scss'; import styles from './ChatContainer.module.scss';
import { ChatMessage } from '../../../interfaces/chat-message.model'; import { ChatMessage } from '../../../interfaces/chat-message.model';
import { ChatTextField, ChatUserMessage } from '..'; import { ChatUserMessage } from '../ChatUserMessage/ChatUserMessage';
import ChatModeratorNotification from '../ChatModeratorNotification/ChatModeratorNotification'; import { ChatTextField } from '../ChatTextField/ChatTextField';
import { ChatModeratorNotification } from '../ChatModeratorNotification/ChatModeratorNotification';
// import ChatActionMessage from '../ChatAction/ChatActionMessage'; // import ChatActionMessage from '../ChatAction/ChatActionMessage';
import ChatSystemMessage from '../ChatSystemMessage/ChatSystemMessage'; import { ChatSystemMessage } from '../ChatSystemMessage/ChatSystemMessage';
import ChatJoinMessage from '../ChatJoinMessage/ChatJoinMessage'; import { ChatJoinMessage } from '../ChatJoinMessage/ChatJoinMessage';
interface Props { export type ChatContainerProps = {
messages: ChatMessage[]; messages: ChatMessage[];
usernameToHighlight: string; usernameToHighlight: string;
chatUserId: string; chatUserId: string;
isModerator: boolean; isModerator: boolean;
showInput?: boolean; showInput?: boolean;
height?: string; height?: string;
};
function shouldCollapseMessages(messages: ChatMessage[], index: number): boolean {
if (messages.length < 2) {
return false;
}
const message = messages[index];
const {
user: { id },
} = message;
const lastMessage = messages[index - 1];
if (lastMessage?.type !== MessageType.CHAT) {
return false;
}
if (!lastMessage.timestamp || !message.timestamp) {
return false;
}
const maxTimestampDelta = 1000 * 60 * 2; // 2 minutes
const lastTimestamp = new Date(lastMessage.timestamp).getTime();
const thisTimestamp = new Date(message.timestamp).getTime();
if (thisTimestamp - lastTimestamp > maxTimestampDelta) {
return false;
}
return id === lastMessage?.user.id;
} }
export default function ChatContainer(props: Props) { function checkIsModerator(message) {
const { messages, usernameToHighlight, chatUserId, isModerator, showInput, height } = props; const { user } = message;
const { scopes } = user;
if (!scopes || scopes.length === 0) {
return false;
}
return scopes.includes('MODERATOR');
}
export const ChatContainer: FC<ChatContainerProps> = ({
messages,
usernameToHighlight,
chatUserId,
isModerator,
showInput,
height,
}) => {
const [atBottom, setAtBottom] = useState(false); const [atBottom, setAtBottom] = useState(false);
// const [showButton, setShowButton] = useState(false); // const [showButton, setShowButton] = useState(false);
const chatContainerRef = useRef(null); const chatContainerRef = useRef(null);
@ -38,13 +83,13 @@ export default function ChatContainer(props: Props) {
const color = `var(--theme-color-users-${displayColor})`; const color = `var(--theme-color-users-${displayColor})`;
return ( return (
<div className={s.nameChangeView}> <div className={styles.nameChangeView}>
<div style={{ marginRight: 5, height: 'max-content', margin: 'auto 5px auto 0' }}> <div style={{ marginRight: 5, height: 'max-content', margin: 'auto 5px auto 0' }}>
<EditFilled /> <EditFilled />
</div> </div>
<div className={s.nameChangeText}> <div className={styles.nameChangeText}>
<span style={{ color }}>{oldName}</span> <span style={{ color }}>{oldName}</span>
<span className={s.plain}> is now known as </span> <span className={styles.plain}> is now known as </span>
<span style={{ color }}>{displayName}</span> <span style={{ color }}>{displayName}</span>
</div> </div>
</div> </div>
@ -129,7 +174,7 @@ export default function ChatContainer(props: Props) {
atBottomStateChange={bottom => setAtBottom(bottom)} atBottomStateChange={bottom => setAtBottom(bottom)}
/> />
{!atBottom && ( {!atBottom && (
<div className={s.toBottomWrap}> <div className={styles.toBottomWrap}>
<Button <Button
type="default" type="default"
icon={<VerticalAlignBottomOutlined />} icon={<VerticalAlignBottomOutlined />}
@ -161,46 +206,7 @@ export default function ChatContainer(props: Props) {
{showInput && <ChatTextField />} {showInput && <ChatTextField />}
</div> </div>
); );
} };
function shouldCollapseMessages(messages: ChatMessage[], index: number): boolean {
if (messages.length < 2) {
return false;
}
const message = messages[index];
const {
user: { id },
} = message;
const lastMessage = messages[index - 1];
if (lastMessage?.type !== MessageType.CHAT) {
return false;
}
if (!lastMessage.timestamp || !message.timestamp) {
return false;
}
const maxTimestampDelta = 1000 * 60 * 2; // 2 minutes
const lastTimestamp = new Date(lastMessage.timestamp).getTime();
const thisTimestamp = new Date(message.timestamp).getTime();
if (thisTimestamp - lastTimestamp > maxTimestampDelta) {
return false;
}
return id === lastMessage?.user.id;
}
function checkIsModerator(message) {
const { user } = message;
const { scopes } = user;
if (!scopes || scopes.length === 0) {
return false;
}
return scopes.includes('MODERATOR');
}
ChatContainer.defaultProps = { ChatContainer.defaultProps = {
showInput: true, showInput: true,

View file

@ -1 +0,0 @@
export { default } from './ChatContainer';

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import ChatJoinMessage from './ChatJoinMessage'; import { ChatJoinMessage } from './ChatJoinMessage';
import Mock from '../../../stories/assets/mocks/chatmessage-action.png'; import Mock from '../../../stories/assets/mocks/chatmessage-action.png';
export default { export default {

View file

@ -1,18 +1,22 @@
import s from './ChatJoinMessage.module.scss'; import { FC } from 'react';
import ChatUserBadge from '../ChatUserBadge/ChatUserBadge'; import styles from './ChatJoinMessage.module.scss';
import { ChatUserBadge } from '../ChatUserBadge/ChatUserBadge';
interface Props { export type ChatJoinMessageProps = {
isAuthorModerator: boolean; isAuthorModerator: boolean;
userColor: number; userColor: number;
displayName: string; displayName: string;
} };
export default function ChatJoinMessage(props: Props) { export const ChatJoinMessage: FC<ChatJoinMessageProps> = ({
const { isAuthorModerator, userColor, displayName } = props; isAuthorModerator,
userColor,
displayName,
}) => {
const color = `var(--theme-user-colors-${userColor})`; const color = `var(--theme-user-colors-${userColor})`;
return ( return (
<div className={s.join}> <div className={styles.join}>
<span style={{ color }}> <span style={{ color }}>
{displayName} {displayName}
{isAuthorModerator && ( {isAuthorModerator && (
@ -24,4 +28,4 @@ export default function ChatJoinMessage(props: Props) {
joined the chat. joined the chat.
</div> </div>
); );
} };

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import ChatModerationActionMenu from './ChatModerationActionMenu'; import { ChatModerationActionMenu } from './ChatModerationActionMenu';
const mocks = { const mocks = {
mocks: [ mocks: [
@ -82,7 +82,7 @@ export default {
} as ComponentMeta<typeof ChatModerationActionMenu>; } as ComponentMeta<typeof ChatModerationActionMenu>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const Template: ComponentStory<typeof ChatModerationActionMenu> = args => ( const Template: ComponentStory<typeof ChatModerationActionMenu> = () => (
<RecoilRoot> <RecoilRoot>
<ChatModerationActionMenu <ChatModerationActionMenu
accessToken="abc123" accessToken="abc123"

View file

@ -5,22 +5,26 @@ import {
SmallDashOutlined, SmallDashOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Dropdown, Menu, MenuProps, Space, Modal, message } from 'antd'; import { Dropdown, Menu, MenuProps, Space, Modal, message } from 'antd';
import { useState } from 'react'; import { FC, useState } from 'react';
import ChatModerationDetailsModal from '../ChatModerationDetailsModal/ChatModerationDetailsModal'; import { ChatModerationDetailsModal } from '../ChatModerationDetailsModal/ChatModerationDetailsModal';
import s from './ChatModerationActionMenu.module.scss'; import styles from './ChatModerationActionMenu.module.scss';
import ChatModeration from '../../../services/moderation-service'; import ChatModeration from '../../../services/moderation-service';
const { confirm } = Modal; const { confirm } = Modal;
interface Props { export type ChatModerationActionMenuProps = {
accessToken: string; accessToken: string;
messageID: string; messageID: string;
userID: string; userID: string;
userDisplayName: string; userDisplayName: string;
} };
export default function ChatModerationActionMenu(props: Props) { export const ChatModerationActionMenu: FC<ChatModerationActionMenuProps> = ({
const { messageID, userID, userDisplayName, accessToken } = props; messageID,
userID,
userDisplayName,
accessToken,
}) => {
const [showUserDetailsModal, setShowUserDetailsModal] = useState(false); const [showUserDetailsModal, setShowUserDetailsModal] = useState(false);
const handleBanUser = async () => { const handleBanUser = async () => {
@ -78,7 +82,7 @@ export default function ChatModerationActionMenu(props: Props) {
{ {
label: ( label: (
<div> <div>
<span className={s.icon}> <span className={styles.icon}>
<EyeInvisibleOutlined /> <EyeInvisibleOutlined />
</span> </span>
Hide Message Hide Message
@ -89,7 +93,7 @@ export default function ChatModerationActionMenu(props: Props) {
{ {
label: ( label: (
<div> <div>
<span className={s.icon}> <span className={styles.icon}>
<CloseCircleOutlined /> <CloseCircleOutlined />
</span> </span>
Ban User Ban User
@ -127,4 +131,4 @@ export default function ChatModerationActionMenu(props: Props) {
</Modal> </Modal>
</> </>
); );
} };

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import ChatModerationDetailsModal from './ChatModerationDetailsModal'; import { ChatModerationDetailsModal } from './ChatModerationDetailsModal';
const mocks = { const mocks = {
mocks: [ mocks: [
@ -82,7 +82,7 @@ export default {
} as ComponentMeta<typeof ChatModerationDetailsModal>; } as ComponentMeta<typeof ChatModerationDetailsModal>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const Template: ComponentStory<typeof ChatModerationDetailsModal> = args => ( const Template: ComponentStory<typeof ChatModerationDetailsModal> = () => (
<RecoilRoot> <RecoilRoot>
<ChatModerationDetailsModal userId="testuser123" accessToken="fakeaccesstoken4839" /> <ChatModerationDetailsModal userId="testuser123" accessToken="fakeaccesstoken4839" />
</RecoilRoot> </RecoilRoot>

View file

@ -1,12 +1,12 @@
import { Button, Col, Row, Spin } from 'antd'; import { Button, Col, Row, Spin } from 'antd';
import { useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import ChatModeration from '../../../services/moderation-service'; import ChatModeration from '../../../services/moderation-service';
import s from './ChatModerationDetailsModal.module.scss'; import styles from './ChatModerationDetailsModal.module.scss';
interface Props { export type ChatModerationDetailsModalProps = {
userId: string; userId: string;
accessToken: string; accessToken: string;
} };
export interface UserDetails { export interface UserDetails {
user: User; user: User;
@ -91,7 +91,7 @@ const UserColorBlock = ({ color }) => {
<Row justify="space-around" align="middle"> <Row justify="space-around" align="middle">
<Col span={12}>Color</Col> <Col span={12}>Color</Col>
<Col span={12}> <Col span={12}>
<div className={s.colorBlock} style={{ backgroundColor: bg }}> <div className={styles.colorBlock} style={{ backgroundColor: bg }}>
{color} {color}
</div> </div>
</Col> </Col>
@ -99,8 +99,10 @@ const UserColorBlock = ({ color }) => {
); );
}; };
export default function ChatModerationDetailsModal(props: Props) { export const ChatModerationDetailsModal: FC<ChatModerationDetailsModalProps> = ({
const { userId, accessToken } = props; userId,
accessToken,
}) => {
const [userDetails, setUserDetails] = useState<UserDetails | null>(null); const [userDetails, setUserDetails] = useState<UserDetails | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -127,7 +129,7 @@ export default function ChatModerationDetailsModal(props: Props) {
user; user;
return ( return (
<div className={s.modalContainer}> <div className={styles.modalContainer}>
<Spin spinning={loading}> <Spin spinning={loading}>
<h1>{displayName}</h1> <h1>{displayName}</h1>
<Row justify="space-around" align="middle"> <Row justify="space-around" align="middle">
@ -161,7 +163,7 @@ export default function ChatModerationDetailsModal(props: Props) {
<div> <div>
<h1>Recent Chat Messages</h1> <h1>Recent Chat Messages</h1>
<div className={s.chatHistory}> <div className={styles.chatHistory}>
{messages.map(message => ( {messages.map(message => (
<ChatMessageRow <ChatMessageRow
key={message.id} key={message.id}
@ -176,4 +178,4 @@ export default function ChatModerationDetailsModal(props: Props) {
</Spin> </Spin>
</div> </div>
); );
} };

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import ChatModeratorNotification from './ChatModeratorNotification'; import { ChatModeratorNotification } from './ChatModeratorNotification';
export default { export default {
title: 'owncast/Chat/Messages/Moderation Role Notification', title: 'owncast/Chat/Messages/Moderation Role Notification',

View file

@ -1,12 +1,9 @@
import s from './ChatModeratorNotification.module.scss'; import styles from './ChatModeratorNotification.module.scss';
import Icon from '../../../assets/images/moderator.svg'; import Icon from '../../../assets/images/moderator.svg';
// eslint-disable-next-line @typescript-eslint/no-unused-vars export const ChatModeratorNotification = () => (
export default function ModeratorNotification() { <div className={styles.chatModerationNotification}>
return ( <Icon className={styles.icon} />
<div className={s.chatModerationNotification}> You are now a moderator.
<Icon className={s.icon} /> </div>
You are now a moderator. );
</div>
);
}

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import ChatSocialMessage from './ChatSocialMessage'; import { ChatSocialMessage } from './ChatSocialMessage';
export default { export default {
title: 'owncast/Chat/Messages/Social-fediverse event', title: 'owncast/Chat/Messages/Social-fediverse event',

View file

@ -1,11 +1,11 @@
/* eslint-disable react/no-unused-prop-types */ /* eslint-disable react/no-unused-prop-types */
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
// TODO remove unused props
import { FC } from 'react';
import { ChatMessage } from '../../../interfaces/chat-message.model'; import { ChatMessage } from '../../../interfaces/chat-message.model';
interface Props { export interface ChatSocialMessageProps {
message: ChatMessage; message: ChatMessage;
} }
export default function ChatSocialMessage(props: Props) { export const ChatSocialMessage: FC<ChatSocialMessageProps> = () => <div>Component goes here</div>;
return <div>Component goes here</div>;
}

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import ChatSystemMessage from './ChatSystemMessage'; import { ChatSystemMessage } from './ChatSystemMessage';
import Mock from '../../../stories/assets/mocks/chatmessage-system.png'; import Mock from '../../../stories/assets/mocks/chatmessage-system.png';
import { ChatMessage } from '../../../interfaces/chat-message.model'; import { ChatMessage } from '../../../interfaces/chat-message.model';

View file

@ -1,25 +1,27 @@
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
import { Highlight } from 'react-highlighter-ts'; import { Highlight } from 'react-highlighter-ts';
import { FC } from 'react';
import { ChatMessage } from '../../../interfaces/chat-message.model'; import { ChatMessage } from '../../../interfaces/chat-message.model';
import s from './ChatSystemMessage.module.scss'; import styles from './ChatSystemMessage.module.scss';
interface Props { export type ChatSystemMessageProps = {
message: ChatMessage; message: ChatMessage;
highlightString: string; highlightString: string;
} };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function ChatSystemMessage({ message, highlightString }: Props) {
const { body, user } = message;
const { displayName } = user;
return ( export const ChatSystemMessage: FC<ChatSystemMessageProps> = ({
<div className={s.chatSystemMessage}> message: {
<div className={s.user}> body,
<span className={s.userName}>{displayName}</span> user: { displayName },
</div> },
<Highlight search={highlightString}> highlightString,
<div className={s.message} dangerouslySetInnerHTML={{ __html: body }} /> }) => (
</Highlight> <div className={styles.chatSystemMessage}>
<div className={styles.user}>
<span className={styles.userName}>{displayName}</span>
</div> </div>
); <Highlight search={highlightString}>
} <div className={styles.message} dangerouslySetInnerHTML={{ __html: body }} />
</Highlight>
</div>
);

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import ChatTextField from './ChatTextField'; import { ChatTextField } from './ChatTextField';
import Mockup from '../../../stories/assets/mocks/chatinput-mock.png'; import Mockup from '../../../stories/assets/mocks/chatinput-mock.png';
const mockResponse = JSON.parse( const mockResponse = JSON.parse(

View file

@ -1,14 +1,14 @@
import { SendOutlined, SmileOutlined } from '@ant-design/icons'; import { SendOutlined, SmileOutlined } from '@ant-design/icons';
import { Button, Popover } from 'antd'; import { Button, Popover } from 'antd';
import React, { useMemo, useState } from 'react'; import React, { FC, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { Editor, Node, Path, Transforms, createEditor, BaseEditor, Text, Descendant } from 'slate'; import { Transforms, createEditor, BaseEditor, Text, Descendant, Editor, Node, Path } from 'slate';
import { Slate, Editable, withReact, ReactEditor, useSelected, useFocused } from 'slate-react'; import { Slate, Editable, withReact, ReactEditor, useSelected, useFocused } from 'slate-react';
import EmojiPicker from './EmojiPicker'; import { EmojiPicker } from './EmojiPicker';
import WebsocketService from '../../../services/websocket-service'; import WebsocketService from '../../../services/websocket-service';
import { websocketServiceAtom } from '../../stores/ClientConfigStore'; import { websocketServiceAtom } from '../../stores/ClientConfigStore';
import { MessageType } from '../../../interfaces/socket-events'; import { MessageType } from '../../../interfaces/socket-events';
import style from './ChatTextField.module.scss'; import styles from './ChatTextField.module.scss';
type CustomElement = { type: 'paragraph' | 'span'; children: CustomText[] } | ImageNode; type CustomElement = { type: 'paragraph' | 'span'; children: CustomText[] } | ImageNode;
type CustomText = { text: string }; type CustomText = { text: string };
@ -90,7 +90,9 @@ const serialize = node => {
} }
}; };
export default function ChatTextField() { export type ChatTextFieldProps = {};
export const ChatTextField: FC<ChatTextFieldProps> = () => {
const [showEmojis, setShowEmojis] = useState(false); const [showEmojis, setShowEmojis] = useState(false);
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom); const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
const editor = useMemo(() => withReact(withImages(createEditor())), []); const editor = useMemo(() => withReact(withImages(createEditor())), []);
@ -196,14 +198,13 @@ export default function ChatTextField() {
return ( return (
<div> <div>
<div className={style.root}> <div className={styles.root}>
<Slate editor={editor} value={defaultEditorValue}> <Slate editor={editor} value={defaultEditorValue}>
<Editable <Editable
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
renderElement={renderElement} renderElement={renderElement}
placeholder="Chat message goes here..." placeholder="Chat message goes here..."
style={{ width: '100%' }} style={{ width: '100%' }}
// onChange={change => setValue(change.value)}
autoFocus autoFocus
/> />
<Popover <Popover
@ -221,14 +222,14 @@ export default function ChatTextField() {
<button <button
type="button" type="button"
className={style.emojiButton} className={styles.emojiButton}
title="Emoji picker button" title="Emoji picker button"
onClick={() => setShowEmojis(!showEmojis)} onClick={() => setShowEmojis(!showEmojis)}
> >
<SmileOutlined /> <SmileOutlined />
</button> </button>
<Button <Button
className={style.sendButton} className={styles.sendButton}
size="large" size="large"
type="ghost" type="ghost"
icon={<SendOutlined />} icon={<SendOutlined />}
@ -237,4 +238,4 @@ export default function ChatTextField() {
</div> </div>
</div> </div>
); );
} };

View file

@ -9,7 +9,7 @@ interface Props {
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function EmojiPicker(props: Props) { export const EmojiPicker = (props: Props) => {
const [customEmoji, setCustomEmoji] = useState([]); const [customEmoji, setCustomEmoji] = useState([]);
const { onEmojiSelect, onCustomEmojiSelect } = props; const { onEmojiSelect, onCustomEmojiSelect } = props;
const ref = useRef(); const ref = useRef();
@ -54,4 +54,4 @@ export default function EmojiPicker(props: Props) {
}, [customEmoji]); }, [customEmoji]);
return <div ref={ref} />; return <div ref={ref} />;
} };

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import ChatUserBadge from './ChatUserBadge'; import { ChatUserBadge } from './ChatUserBadge';
export default { export default {
title: 'owncast/Chat/Messages/User Flag', title: 'owncast/Chat/Messages/User Flag',

View file

@ -1,19 +1,18 @@
import React from 'react'; import React, { FC } from 'react';
import s from './ChatUserBadge.module.scss'; import styles from './ChatUserBadge.module.scss';
interface Props { export type ChatUserBadgeProps = {
badge: React.ReactNode; badge: React.ReactNode;
userColor: number; userColor: number;
} };
export default function ChatUserBadge(props: Props) { export const ChatUserBadge: FC<ChatUserBadgeProps> = ({ badge, userColor }) => {
const { badge, userColor } = props;
const color = `var(--theme-user-colors-${userColor})`; const color = `var(--theme-user-colors-${userColor})`;
const style = { color, borderColor: color }; const style = { color, borderColor: color };
return ( return (
<span style={style} className={s.badge}> <span style={style} className={styles.badge}>
{badge} {badge}
</span> </span>
); );
} };

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import ChatUserMessage from './index'; import { ChatUserMessage } from './ChatUserMessage';
import { ChatMessage } from '../../../interfaces/chat-message.model'; import { ChatMessage } from '../../../interfaces/chat-message.model';
import Mock from '../../../stories/assets/mocks/chatmessage-user.png'; import Mock from '../../../stories/assets/mocks/chatmessage-user.png';

View file

@ -1,19 +1,19 @@
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
import { useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { Highlight } from 'react-highlighter-ts'; import { Highlight } from 'react-highlighter-ts';
import he from 'he'; import he from 'he';
import cn from 'classnames'; import cn from 'classnames';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { LinkOutlined } from '@ant-design/icons'; import { LinkOutlined } from '@ant-design/icons';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import s from './ChatUserMessage.module.scss'; import styles from './ChatUserMessage.module.scss';
import { formatTimestamp } from './messageFmt'; import { formatTimestamp } from './messageFmt';
import { ChatMessage } from '../../../interfaces/chat-message.model'; import { ChatMessage } from '../../../interfaces/chat-message.model';
import ChatModerationActionMenu from '../ChatModerationActionMenu/ChatModerationActionMenu'; import { ChatModerationActionMenu } from '../ChatModerationActionMenu/ChatModerationActionMenu';
import ChatUserBadge from '../ChatUserBadge/ChatUserBadge'; import { ChatUserBadge } from '../ChatUserBadge/ChatUserBadge';
import { accessTokenAtom } from '../../stores/ClientConfigStore'; import { accessTokenAtom } from '../../stores/ClientConfigStore';
interface Props { export type ChatUserMessageProps = {
message: ChatMessage; message: ChatMessage;
showModeratorMenu: boolean; showModeratorMenu: boolean;
highlightString: string; highlightString: string;
@ -21,9 +21,9 @@ interface Props {
sameUserAsLast: boolean; sameUserAsLast: boolean;
isAuthorModerator: boolean; isAuthorModerator: boolean;
isAuthorAuthenticated: boolean; isAuthorAuthenticated: boolean;
} };
export default function ChatUserMessage({ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
message, message,
highlightString, highlightString,
showModeratorMenu, showModeratorMenu,
@ -31,7 +31,7 @@ export default function ChatUserMessage({
sameUserAsLast, sameUserAsLast,
isAuthorModerator, isAuthorModerator,
isAuthorAuthenticated, isAuthorAuthenticated,
}: Props) { }) => {
const { id: messageId, body, user, timestamp } = message; const { id: messageId, body, user, timestamp } = message;
const { id: userId, displayName, displayColor } = user; const { id: userId, displayName, displayColor } = user;
const accessToken = useRecoilValue<string>(accessTokenAtom); const accessToken = useRecoilValue<string>(accessTokenAtom);
@ -59,29 +59,32 @@ export default function ChatUserMessage({
}, [message]); }, [message]);
return ( return (
<div className={cn(s.messagePadding, sameUserAsLast && s.messagePaddingCollapsed)}> <div className={cn(styles.messagePadding, sameUserAsLast && styles.messagePaddingCollapsed)}>
<div <div
className={cn(s.root, { className={cn(styles.root, {
[s.ownMessage]: sentBySelf, [styles.ownMessage]: sentBySelf,
})} })}
style={{ borderColor: color }} style={{ borderColor: color }}
> >
{!sameUserAsLast && ( {!sameUserAsLast && (
<Tooltip title="user info goes here" placement="topLeft" mouseEnterDelay={1}> <Tooltip title="user info goes here" placement="topLeft" mouseEnterDelay={1}>
<div className={s.user} style={{ color }}> <div className={styles.user} style={{ color }}>
<span className={s.userName}>{displayName}</span> <span className={styles.userName}>{displayName}</span>
<span>{badgeNodes}</span> <span>{badgeNodes}</span>
</div> </div>
</Tooltip> </Tooltip>
)} )}
<Tooltip title={formattedTimestamp} mouseEnterDelay={1}> <Tooltip title={formattedTimestamp} mouseEnterDelay={1}>
<Highlight search={highlightString}> <Highlight search={highlightString}>
<div className={s.message} dangerouslySetInnerHTML={{ __html: formattedMessage }} /> <div
className={styles.message}
dangerouslySetInnerHTML={{ __html: formattedMessage }}
/>
</Highlight> </Highlight>
</Tooltip> </Tooltip>
{showModeratorMenu && ( {showModeratorMenu && (
<div className={s.modMenuWrapper}> <div className={styles.modMenuWrapper}>
<ChatModerationActionMenu <ChatModerationActionMenu
messageID={messageId} messageID={messageId}
accessToken={accessToken} accessToken={accessToken}
@ -90,9 +93,9 @@ export default function ChatUserMessage({
/> />
</div> </div>
)} )}
<div className={s.customBorder} style={{ color }} /> <div className={styles.customBorder} style={{ color }} />
<div className={s.background} style={{ color }} /> <div className={styles.background} style={{ color }} />
</div> </div>
</div> </div>
); );
} };

View file

@ -1 +0,0 @@
export { default } from './ChatUserMessage';

View file

@ -1,3 +0,0 @@
export { default as ChatContainer } from './ChatContainer';
export { default as ChatUserMessage } from './ChatUserMessage';
export { default as ChatTextField } from './ChatTextField/ChatTextField';

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import ContentHeader from './ContentHeader'; import { ContentHeader } from './ContentHeader';
export default { export default {
title: 'owncast/Components/Content Header', title: 'owncast/Components/Content Header',

View file

@ -1,36 +1,42 @@
import cn from 'classnames'; import cn from 'classnames';
import { FC } from 'react';
import { ServerLogo } from '../../ui'; import { Logo } from '../../ui/Logo/Logo';
import SocialLinks from '../../ui/SocialLinks/SocialLinks'; import { SocialLinks } from '../../ui/SocialLinks/SocialLinks';
import { SocialLink } from '../../../interfaces/social-link.model'; import { SocialLink } from '../../../interfaces/social-link.model';
import s from './ContentHeader.module.scss'; import styles from './ContentHeader.module.scss';
interface Props { export type ContentHeaderProps = {
name: string; name: string;
title: string; title: string;
summary: string; summary: string;
tags: string[]; tags: string[];
links: SocialLink[]; links: SocialLink[];
logo: string; logo: string;
} };
export default function ContentHeader({ name, title, summary, logo, tags, links }: Props) {
return ( export const ContentHeader: FC<ContentHeaderProps> = ({
<div className={s.root}> name,
<div className={s.logoTitleSection}> title,
<div className={s.logo}> summary,
<ServerLogo src={logo} /> logo,
tags,
links,
}) => (
<div className={styles.root}>
<div className={styles.logoTitleSection}>
<div className={styles.logo}>
<Logo src={logo} />
</div>
<div className={styles.titleSection}>
<div className={cn(styles.title, styles.row)}>{name}</div>
<div className={cn(styles.subtitle, styles.row)}>{title || summary}</div>
<div className={cn(styles.tagList, styles.row)}>
{tags.length > 0 && tags.map(tag => <span key={tag}>#{tag}&nbsp;</span>)}
</div> </div>
<div className={s.titleSection}> <div className={cn(styles.socialLinks, styles.row)}>
<div className={cn(s.title, s.row)}>{name}</div> <SocialLinks links={links} />
<div className={cn(s.subtitle, s.row)}>{title || summary}</div>
<div className={cn(s.tagList, s.row)}>
{tags.length > 0 && tags.map(tag => <span key={tag}>#{tag}&nbsp;</span>)}
</div>
<div className={cn(s.socialLinks, s.row)}>
<SocialLinks links={links} />
</div>
</div> </div>
</div> </div>
</div> </div>
); </div>
} );

View file

@ -1 +0,0 @@
export { default } from './ContentHeader';

View file

@ -1 +0,0 @@
export { default } from './Logo';

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import OwncastLogo from './Logo'; import { OwncastLogo } from './OwncastLogo';
export default { export default {
title: 'owncast/Components/Header Logo', title: 'owncast/Components/Header Logo',

View file

@ -1,15 +1,15 @@
import React from 'react'; import React, { FC } from 'react';
import cn from 'classnames'; import cn from 'classnames';
import s from './Logo.module.scss'; import styles from './OwncastLogo.module.scss';
interface Props { export type LogoProps = {
variant: 'simple' | 'contrast'; variant: 'simple' | 'contrast';
} };
export default function Logo({ variant = 'simple' }: Props) { export const OwncastLogo: FC<LogoProps> = ({ variant = 'simple' }) => {
const rootClassName = cn(s.root, { const rootClassName = cn(styles.root, {
[s.simple]: variant === 'simple', [styles.simple]: variant === 'simple',
[s.contrast]: variant === 'contrast', [styles.contrast]: variant === 'contrast',
}); });
return ( return (
@ -169,4 +169,4 @@ export default function Logo({ variant = 'simple' }: Props) {
</svg> </svg>
</div> </div>
); );
} };

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import UserDropdown from './UserDropdown'; import { UserDropdown } from './UserDropdown';
export default { export default {
title: 'owncast/Components/User settings menu', title: 'owncast/Components/User settings menu',

View file

@ -7,24 +7,24 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { useState } from 'react'; import { FC, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import Modal from '../../ui/Modal/Modal'; import { Modal } from '../../ui/Modal/Modal';
import { import {
chatVisibleToggleAtom, chatVisibleToggleAtom,
chatDisplayNameAtom, chatDisplayNameAtom,
appStateAtom, appStateAtom,
} from '../../stores/ClientConfigStore'; } from '../../stores/ClientConfigStore';
import s from './UserDropdown.module.scss'; import styles from './UserDropdown.module.scss';
import NameChangeModal from '../../modals/NameChangeModal/NameChangeModal'; import { NameChangeModal } from '../../modals/NameChangeModal/NameChangeModal';
import { AppStateOptions } from '../../stores/application-state'; import { AppStateOptions } from '../../stores/application-state';
import AuthModal from '../../modals/AuthModal/AuthModal'; import { AuthModal } from '../../modals/AuthModal/AuthModal';
interface Props { export type UserDropdownProps = {
username?: string; username?: string;
} };
export default function UserDropdown({ username: defaultUsername }: Props) { export const UserDropdown: FC<UserDropdownProps> = ({ username: defaultUsername = undefined }) => {
const username = defaultUsername || useRecoilValue(chatDisplayNameAtom); const username = defaultUsername || useRecoilValue(chatDisplayNameAtom);
const [showNameChangeModal, setShowNameChangeModal] = useState<boolean>(false); const [showNameChangeModal, setShowNameChangeModal] = useState<boolean>(false);
const [showAuthModal, setShowAuthModal] = useState<boolean>(false); const [showAuthModal, setShowAuthModal] = useState<boolean>(false);
@ -66,7 +66,7 @@ export default function UserDropdown({ username: defaultUsername }: Props) {
); );
return ( return (
<div className={`${s.root}`}> <div className={`${styles.root}`}>
<Dropdown overlay={menu} trigger={['click']}> <Dropdown overlay={menu} trigger={['click']}>
<Button type="primary" icon={<UserOutlined style={{ marginRight: '.5rem' }} />}> <Button type="primary" icon={<UserOutlined style={{ marginRight: '.5rem' }} />}>
<Space> <Space>
@ -91,8 +91,4 @@ export default function UserDropdown({ username: defaultUsername }: Props) {
</Modal> </Modal>
</div> </div>
); );
}
UserDropdown.defaultProps = {
username: undefined,
}; };

View file

@ -1 +0,0 @@
export { default } from './UserDropdown';

View file

@ -1,2 +0,0 @@
export { default as UserDropdown } from './UserDropdown';
export { default as OwncastLogo } from './Logo';

View file

@ -1,5 +1,5 @@
import { Popconfirm, Select, Typography } from 'antd'; import { Popconfirm, Select, Typography } from 'antd';
import React, { useContext, useEffect, useState } from 'react'; import React, { FC, useContext, useEffect, useState } from 'react';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
import { import {
API_VIDEO_CODEC, API_VIDEO_CODEC,
@ -13,10 +13,11 @@ import {
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
// eslint-disable-next-line react/function-component-definition export type CodecSelectorProps = {};
export default function CodecSelector() {
export const CodecSelector: FC<CodecSelectorProps> = () => {
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { videoCodec, supportedCodecs } = serverConfig || {}; const { videoCodec, supportedCodecs } = serverConfig || {};
@ -170,4 +171,4 @@ export default function CodecSelector() {
</div> </div>
</> </>
); );
} };

View file

@ -1,6 +1,6 @@
// Updating a variant will post ALL the variants in an array as an update to the API. // Updating a variant will post ALL the variants in an array as an update to the API.
import React, { useContext, useState } from 'react'; import React, { FC, useContext, useState } from 'react';
import { Typography, Table, Modal, Button, Alert } from 'antd'; import { Typography, Table, Modal, Button, Alert } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
@ -8,7 +8,7 @@ import { ServerStatusContext } from '../../utils/server-status-context';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
import { UpdateArgs, VideoVariant } from '../../types/config-section'; import { UpdateArgs, VideoVariant } from '../../types/config-section';
import VideoVariantForm from './video-variant-form'; import { VideoVariantForm } from './VideoVariantForm';
import { import {
API_VIDEO_VARIANTS, API_VIDEO_VARIANTS,
DEFAULT_VARIANT_STATE, DEFAULT_VARIANT_STATE,
@ -24,11 +24,11 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
const { Title } = Typography; const { Title } = Typography;
export default function CurrentVariantsTable() { export const CurrentVariantsTable: FC = () => {
const [displayModal, setDisplayModal] = useState(false); const [displayModal, setDisplayModal] = useState(false);
const [modalProcessing, setModalProcessing] = useState(false); const [modalProcessing, setModalProcessing] = useState(false);
const [editId, setEditId] = useState(0); const [editId, setEditId] = useState(0);
@ -242,4 +242,4 @@ export default function CurrentVariantsTable() {
</Button> </Button>
</> </>
); );
} };

View file

@ -1,5 +1,5 @@
// EDIT CUSTOM CSS STYLES // EDIT CUSTOM CSS STYLES
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext, FC } from 'react';
import { Typography, Button } from 'antd'; import { Typography, Button } from 'antd';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
@ -15,14 +15,13 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
import { TextField, TEXTFIELD_TYPE_TEXTAREA } from './TextField';
import TextField, { TEXTFIELD_TYPE_TEXTAREA } from './form-textfield';
import { UpdateArgs } from '../../types/config-section'; import { UpdateArgs } from '../../types/config-section';
const { Title } = Typography; const { Title } = Typography;
export default function EditCustomStyles() { export const EditCustomStyles: FC = () => {
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [hasChanged, setHasChanged] = useState(false); const [hasChanged, setHasChanged] = useState(false);
@ -114,4 +113,4 @@ export default function EditCustomStyles() {
</div> </div>
</div> </div>
); );
} };

View file

@ -1,11 +1,10 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect, FC } from 'react';
import { Typography } from 'antd'; import { Typography } from 'antd';
import {
import TextFieldWithSubmit, { TextFieldWithSubmit,
TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_TEXTAREA,
TEXTFIELD_TYPE_URL, TEXTFIELD_TYPE_URL,
} from './form-textfield-with-submit'; } from './TextFieldWithSubmit';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { import {
postConfigUpdateToAPI, postConfigUpdateToAPI,
@ -18,14 +17,13 @@ import {
FIELD_PROPS_NSFW, FIELD_PROPS_NSFW,
FIELD_PROPS_HIDE_VIEWER_COUNT, FIELD_PROPS_HIDE_VIEWER_COUNT,
} from '../../utils/config-constants'; } from '../../utils/config-constants';
import { UpdateArgs } from '../../types/config-section'; import { UpdateArgs } from '../../types/config-section';
import ToggleSwitch from './form-toggleswitch'; import { ToggleSwitch } from './ToggleSwitch';
import EditLogo from './edit-logo'; import { EditLogo } from './EditLogo';
const { Title } = Typography; const { Title } = Typography;
export default function EditInstanceDetails() { export const EditInstanceDetails: FC = () => {
const [formDataValues, setFormDataValues] = useState(null); const [formDataValues, setFormDataValues] = useState(null);
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig } = serverStatusData || {}; const { serverConfig } = serverStatusData || {};
@ -163,4 +161,4 @@ export default function EditInstanceDetails() {
</div> </div>
</div> </div>
); );
} };

View file

@ -1,17 +1,10 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { Button, Tooltip, Collapse, Typography } from 'antd'; import { Button, Tooltip, Collapse, Typography } from 'antd';
import { CopyOutlined, RedoOutlined } from '@ant-design/icons'; import { CopyOutlined, RedoOutlined } from '@ant-design/icons';
import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD, TEXTFIELD_TYPE_URL } from './TextField';
import { import { TextFieldWithSubmit } from './TextFieldWithSubmit';
TEXTFIELD_TYPE_NUMBER,
TEXTFIELD_TYPE_PASSWORD,
TEXTFIELD_TYPE_URL,
} from './form-textfield';
import TextFieldWithSubmit from './form-textfield-with-submit';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
import { import {
TEXTFIELD_PROPS_FFMPEG, TEXTFIELD_PROPS_FFMPEG,
TEXTFIELD_PROPS_RTMP_PORT, TEXTFIELD_PROPS_RTMP_PORT,
@ -19,13 +12,12 @@ import {
TEXTFIELD_PROPS_STREAM_KEY, TEXTFIELD_PROPS_STREAM_KEY,
TEXTFIELD_PROPS_WEB_PORT, TEXTFIELD_PROPS_WEB_PORT,
} from '../../utils/config-constants'; } from '../../utils/config-constants';
import { UpdateArgs } from '../../types/config-section'; import { UpdateArgs } from '../../types/config-section';
import ResetYP from './reset-yp'; import { ResetYP } from './ResetYP';
const { Panel } = Collapse; const { Panel } = Collapse;
export default function EditInstanceDetails() { export const EditInstanceDetails = () => {
const [formDataValues, setFormDataValues] = useState(null); const [formDataValues, setFormDataValues] = useState(null);
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { setMessage } = useContext(AlertMessageContext); const { setMessage } = useContext(AlertMessageContext);
@ -164,4 +156,4 @@ export default function EditInstanceDetails() {
</Collapse> </Collapse>
</div> </div>
); );
} };

View file

@ -1,14 +1,13 @@
/* eslint-disable react/no-array-index-key */ /* eslint-disable react/no-array-index-key */
import React, { useContext, useState, useEffect } from 'react'; import React, { useContext, useState, useEffect, FC } from 'react';
import { Typography, Tag } from 'antd'; import { Typography, Tag } from 'antd';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { import {
FIELD_PROPS_TAGS, FIELD_PROPS_TAGS,
RESET_TIMEOUT, RESET_TIMEOUT,
postConfigUpdateToAPI, postConfigUpdateToAPI,
} from '../../utils/config-constants'; } from '../../utils/config-constants';
import TextField from './form-textfield'; import { TextField } from './TextField';
import { UpdateArgs } from '../../types/config-section'; import { UpdateArgs } from '../../types/config-section';
import { import {
createInputStatus, createInputStatus,
@ -18,11 +17,11 @@ import {
STATUS_SUCCESS, STATUS_SUCCESS,
STATUS_WARNING, STATUS_WARNING,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import { TAG_COLOR } from './edit-string-array'; import { TAG_COLOR } from './EditValueArray';
const { Title } = Typography; const { Title } = Typography;
export default function EditInstanceTags() { export const EditInstanceTags: FC = () => {
const [newTagInput, setNewTagInput] = useState<string>(''); const [newTagInput, setNewTagInput] = useState<string>('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@ -136,4 +135,4 @@ export default function EditInstanceTags() {
</div> </div>
</div> </div>
); );
} };

View file

@ -1,8 +1,8 @@
import { Button, Upload } from 'antd'; import { Button, Upload } from 'antd';
import { RcFile } from 'antd/lib/upload/interface'; import { RcFile } from 'antd/lib/upload/interface';
import { LoadingOutlined, UploadOutlined } from '@ant-design/icons'; import { LoadingOutlined, UploadOutlined } from '@ant-design/icons';
import React, { useState, useContext } from 'react'; import React, { useState, useContext, FC } from 'react';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { import {
postConfigUpdateToAPI, postConfigUpdateToAPI,
@ -26,7 +26,7 @@ function getBase64(img: File | Blob, callback: (imageUrl: string | ArrayBuffer)
reader.readAsDataURL(img); reader.readAsDataURL(img);
} }
export default function EditLogo() { export const EditLogo: FC = () => {
const [logoUrl, setlogoUrl] = useState(null); const [logoUrl, setlogoUrl] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [logoCachedbuster, setLogoCacheBuster] = useState(0); const [logoCachedbuster, setLogoCacheBuster] = useState(0);
@ -125,4 +125,4 @@ export default function EditLogo() {
</div> </div>
</div> </div>
); );
} };

View file

@ -1,5 +1,5 @@
// EDIT CUSTOM DETAILS ON YOUR PAGE // EDIT CUSTOM DETAILS ON YOUR PAGE
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext, FC } from 'react';
import { Typography, Button } from 'antd'; import { Typography, Button } from 'antd';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
@ -17,7 +17,7 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
import 'react-markdown-editor-lite/lib/index.css'; import 'react-markdown-editor-lite/lib/index.css';
@ -28,7 +28,7 @@ const MdEditor = dynamic(() => import('react-markdown-editor-lite'), {
const { Title } = Typography; const { Title } = Typography;
export default function EditPageContent() { export const EditPageContent: FC = () => {
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [hasChanged, setHasChanged] = useState(false); const [hasChanged, setHasChanged] = useState(false);
@ -122,4 +122,4 @@ export default function EditPageContent() {
</div> </div>
</div> </div>
); );
} };

View file

@ -1,8 +1,8 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect, FC } from 'react';
import { Typography, Table, Button, Modal, Input } from 'antd'; import { Typography, Table, Button, Modal, Input } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import SocialDropdown from './social-icons-dropdown'; import { SocialDropdown } from './SocialDropdown';
import { fetchData, SOCIAL_PLATFORMS_LIST, NEXT_PUBLIC_API_HOST } from '../../utils/apis'; import { fetchData, SOCIAL_PLATFORMS_LIST, NEXT_PUBLIC_API_HOST } from '../../utils/apis';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { import {
@ -14,13 +14,13 @@ import {
} from '../../utils/config-constants'; } from '../../utils/config-constants';
import { SocialHandle, UpdateArgs } from '../../types/config-section'; import { SocialHandle, UpdateArgs } from '../../types/config-section';
import isValidUrl, { DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/urls'; import isValidUrl, { DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/urls';
import TextField from './form-textfield'; import { TextField } from './TextField';
import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../utils/input-statuses'; import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
const { Title } = Typography; const { Title } = Typography;
export default function EditSocialLinks() { export const EditSocialLinks: FC = () => {
const [availableIconsList, setAvailableIconsList] = useState([]); const [availableIconsList, setAvailableIconsList] = useState([]);
const [currentSocialHandles, setCurrentSocialHandles] = useState([]); const [currentSocialHandles, setCurrentSocialHandles] = useState([]);
@ -316,4 +316,4 @@ export default function EditSocialLinks() {
</Button> </Button>
</div> </div>
); );
} };

View file

@ -1,6 +1,6 @@
import { Button, Collapse } from 'antd'; import { Button, Collapse } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useContext, useState, useEffect } from 'react'; import React, { useContext, useState, useEffect, FC } from 'react';
import { UpdateArgs } from '../../types/config-section'; import { UpdateArgs } from '../../types/config-section';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
@ -18,10 +18,10 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import TextField from './form-textfield'; import { TextField } from './TextField';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
import isValidUrl from '../../utils/urls'; import isValidUrl from '../../utils/urls';
import ToggleSwitch from './form-toggleswitch'; import { ToggleSwitch } from './ToggleSwitch';
const { Panel } = Collapse; const { Panel } = Collapse;
@ -63,7 +63,7 @@ function checkSaveable(formValues: any, currentValues: any) {
return false; return false;
} }
export default function EditStorage() { export const EditStorage: FC = () => {
const [formDataValues, setFormDataValues] = useState(null); const [formDataValues, setFormDataValues] = useState(null);
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@ -254,4 +254,4 @@ export default function EditStorage() {
</div> </div>
</div> </div>
); );
} };

View file

@ -1,17 +1,17 @@
/* eslint-disable react/no-array-index-key */ /* eslint-disable react/no-array-index-key */
import React, { useState } from 'react'; import React, { FC, useState } from 'react';
import { Typography, Tag } from 'antd'; import { Typography, Tag } from 'antd';
import TextField from './form-textfield'; import { TextField } from './TextField';
import { UpdateArgs } from '../../types/config-section'; import { UpdateArgs } from '../../types/config-section';
import { StatusState } from '../../utils/input-statuses'; import { StatusState } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
const { Title } = Typography; const { Title } = Typography;
export const TAG_COLOR = '#5a67d8'; export const TAG_COLOR = '#5a67d8';
interface EditStringArrayProps { export type EditStringArrayProps = {
title: string; title: string;
description?: string; description?: string;
placeholder: string; placeholder: string;
@ -21,21 +21,20 @@ interface EditStringArrayProps {
continuousStatusMessage?: StatusState; continuousStatusMessage?: StatusState;
handleDeleteIndex: (index: number) => void; handleDeleteIndex: (index: number) => void;
handleCreateString: (arg: string) => void; handleCreateString: (arg: string) => void;
} };
export default function EditValueArray(props: EditStringArrayProps) { export const EditValueArray: FC<EditStringArrayProps> = ({
title,
description,
placeholder,
maxLength,
values,
handleDeleteIndex,
handleCreateString,
submitStatus,
continuousStatusMessage,
}) => {
const [newStringInput, setNewStringInput] = useState<string>(''); const [newStringInput, setNewStringInput] = useState<string>('');
const {
title,
description,
placeholder,
maxLength,
values,
handleDeleteIndex,
handleCreateString,
submitStatus,
continuousStatusMessage,
} = props;
const handleInputChange = ({ value }: UpdateArgs) => { const handleInputChange = ({ value }: UpdateArgs) => {
setNewStringInput(value); setNewStringInput(value);
@ -84,7 +83,7 @@ export default function EditValueArray(props: EditStringArrayProps) {
</div> </div>
</div> </div>
); );
} };
EditValueArray.defaultProps = { EditValueArray.defaultProps = {
maxLength: 50, maxLength: 50,

View file

@ -1,15 +1,14 @@
// Note: references to "yp" in the app are likely related to Owncast Directory // Note: references to "yp" in the app are likely related to Owncast Directory
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect, FC } from 'react';
import { Typography } from 'antd'; import { Typography } from 'antd';
import ToggleSwitch from './form-toggleswitch'; import { ToggleSwitch } from './ToggleSwitch';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { FIELD_PROPS_NSFW, FIELD_PROPS_YP } from '../../utils/config-constants'; import { FIELD_PROPS_NSFW, FIELD_PROPS_YP } from '../../utils/config-constants';
const { Title } = Typography; const { Title } = Typography;
export default function EditYPDetails() { export const EditYPDetails: FC = () => {
const [formDataValues, setFormDataValues] = useState(null); const [formDataValues, setFormDataValues] = useState(null);
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
@ -68,4 +67,4 @@ export default function EditYPDetails() {
</div> </div>
</div> </div>
); );
} };

View file

@ -1,12 +1,13 @@
import React from 'react'; import React, { FC } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { StatusState } from '../../utils/input-statuses'; import { StatusState } from '../../utils/input-statuses';
interface FormStatusIndicatorProps { export type FormStatusIndicatorProps = {
status: StatusState; status: StatusState;
} };
export default function FormStatusIndicator({ status }: FormStatusIndicatorProps) {
export const FormStatusIndicator: FC<FormStatusIndicatorProps> = ({ status }) => {
const { type, icon, message } = status || {}; const { type, icon, message } = status || {};
const classes = classNames({ const classes = classNames({
'status-container': true, 'status-container': true,
@ -19,4 +20,5 @@ export default function FormStatusIndicator({ status }: FormStatusIndicatorProps
{message ? <span className="status-message">{message}</span> : null} {message ? <span className="status-message">{message}</span> : null}
</span> </span>
); );
} };
export default FormStatusIndicator;

View file

@ -1,5 +1,5 @@
import { Popconfirm, Button, Typography } from 'antd'; import { Popconfirm, Button, Typography } from 'antd';
import { useContext, useState } from 'react'; import { FC, useContext, useState } from 'react';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
import { API_YP_RESET, fetchData } from '../../utils/apis'; import { API_YP_RESET, fetchData } from '../../utils/apis';
@ -10,9 +10,9 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
export default function ResetYP() { export const ResetYP: FC = () => {
const { setMessage } = useContext(AlertMessageContext); const { setMessage } = useContext(AlertMessageContext);
const [submitStatus, setSubmitStatus] = useState(null); const [submitStatus, setSubmitStatus] = useState(null);
@ -61,4 +61,4 @@ export default function ResetYP() {
</p> </p>
</> </>
); );
} };

View file

@ -1,16 +1,16 @@
import React from 'react'; import React, { FC } from 'react';
import { Select } from 'antd'; import { Select } from 'antd';
import { SocialHandleDropdownItem } from '../../types/config-section'; import { SocialHandleDropdownItem } from '../../types/config-section';
import { NEXT_PUBLIC_API_HOST } from '../../utils/apis'; import { NEXT_PUBLIC_API_HOST } from '../../utils/apis';
import { OTHER_SOCIAL_HANDLE_OPTION } from '../../utils/config-constants'; import { OTHER_SOCIAL_HANDLE_OPTION } from '../../utils/config-constants';
interface DropdownProps { export type DropdownProps = {
iconList: SocialHandleDropdownItem[]; iconList: SocialHandleDropdownItem[];
selectedOption: string; selectedOption: string;
onSelected: any; onSelected: any;
} };
export default function SocialDropdown({ iconList, selectedOption, onSelected }: DropdownProps) { export const SocialDropdown: FC<DropdownProps> = ({ iconList, selectedOption, onSelected }) => {
const handleSelected = (value: string) => { const handleSelected = (value: string) => {
if (onSelected) { if (onSelected) {
onSelected(value); onSelected(value);
@ -62,4 +62,4 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
</div> </div>
</div> </div>
); );
} };

View file

@ -1,10 +1,10 @@
import React from 'react'; import React, { FC } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Input, InputNumber } from 'antd'; import { Input, InputNumber } from 'antd';
import { FieldUpdaterFunc } from '../../types/config-section'; import { FieldUpdaterFunc } from '../../types/config-section';
// import InfoTip from '../info-tip'; // import InfoTip from '../info-tip';
import { StatusState } from '../../utils/input-statuses'; import { StatusState } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
export const TEXTFIELD_TYPE_TEXT = 'default'; export const TEXTFIELD_TYPE_TEXT = 'default';
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
@ -12,7 +12,7 @@ export const TEXTFIELD_TYPE_NUMBER = 'numeric'; // InputNumber
export const TEXTFIELD_TYPE_TEXTAREA = 'textarea'; // Input.TextArea export const TEXTFIELD_TYPE_TEXTAREA = 'textarea'; // Input.TextArea
export const TEXTFIELD_TYPE_URL = 'url'; export const TEXTFIELD_TYPE_URL = 'url';
export interface TextFieldProps { export type TextFieldProps = {
fieldName: string; fieldName: string;
onSubmit?: () => void; onSubmit?: () => void;
@ -33,28 +33,26 @@ export interface TextFieldProps {
value?: string | number; value?: string | number;
onBlur?: FieldUpdaterFunc; onBlur?: FieldUpdaterFunc;
onChange?: FieldUpdaterFunc; onChange?: FieldUpdaterFunc;
} };
export default function TextField(props: TextFieldProps) {
const {
className,
disabled,
fieldName,
label,
maxLength,
onBlur,
onChange,
onPressEnter,
pattern,
placeholder,
required,
status,
tip,
type,
useTrim,
value,
} = props;
export const TextField: FC<TextFieldProps> = ({
className,
disabled,
fieldName,
label,
maxLength,
onBlur,
onChange,
onPressEnter,
pattern,
placeholder,
required,
status,
tip,
type,
useTrim,
value,
}) => {
const handleChange = (e: any) => { const handleChange = (e: any) => {
// if an extra onChange handler was sent in as a prop, let's run that too. // if an extra onChange handler was sent in as a prop, let's run that too.
if (onChange) { if (onChange) {
@ -151,7 +149,8 @@ export default function TextField(props: TextFieldProps) {
</div> </div>
</div> </div>
); );
} };
export default TextField;
TextField.defaultProps = { TextField.defaultProps = {
className: '', className: '',

View file

@ -1,6 +1,6 @@
import { Button } from 'antd'; import { Button } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useContext, useEffect, useState } from 'react'; import React, { FC, useContext, useEffect, useState } from 'react';
import { UpdateArgs } from '../../types/config-section'; import { UpdateArgs } from '../../types/config-section';
import { postConfigUpdateToAPI, RESET_TIMEOUT } from '../../utils/config-constants'; import { postConfigUpdateToAPI, RESET_TIMEOUT } from '../../utils/config-constants';
import { import {
@ -11,8 +11,8 @@ import {
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
import TextField, { TextFieldProps } from './form-textfield'; import { TextField, TextFieldProps } from './TextField';
export const TEXTFIELD_TYPE_TEXT = 'default'; export const TEXTFIELD_TYPE_TEXT = 'default';
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
@ -20,13 +20,20 @@ export const TEXTFIELD_TYPE_NUMBER = 'numeric';
export const TEXTFIELD_TYPE_TEXTAREA = 'textarea'; export const TEXTFIELD_TYPE_TEXTAREA = 'textarea';
export const TEXTFIELD_TYPE_URL = 'url'; export const TEXTFIELD_TYPE_URL = 'url';
interface TextFieldWithSubmitProps extends TextFieldProps { export type TextFieldWithSubmitProps = TextFieldProps & {
apiPath: string; apiPath: string;
configPath?: string; configPath?: string;
initialValue?: string; initialValue?: string;
} };
export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) { export const TextFieldWithSubmit: FC<TextFieldWithSubmitProps> = ({
apiPath,
configPath = '',
initialValue,
useTrim,
useTrimLead,
...textFieldProps // rest of props
}) => {
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [hasChanged, setHasChanged] = useState(false); const [hasChanged, setHasChanged] = useState(false);
@ -36,15 +43,6 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
let resetTimer = null; let resetTimer = null;
const {
apiPath,
configPath = '',
initialValue,
useTrim,
useTrimLead,
...textFieldProps // rest of props
} = props;
const { fieldName, required, tip, status, value, onChange, onSubmit } = textFieldProps; const { fieldName, required, tip, status, value, onChange, onSubmit } = textFieldProps;
// Clear out any validation states and messaging // Clear out any validation states and messaging
@ -150,7 +148,7 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
</div> </div>
</div> </div>
); );
} };
TextFieldWithSubmit.defaultProps = { TextFieldWithSubmit.defaultProps = {
configPath: '', configPath: '',

View file

@ -2,7 +2,7 @@
// This one is styled to match the form-textfield component. // This one is styled to match the form-textfield component.
// If `useSubmit` is true then it will automatically post to the config API onChange. // If `useSubmit` is true then it will automatically post to the config API onChange.
import React, { useState, useContext } from 'react'; import React, { useState, useContext, FC } from 'react';
import { Switch } from 'antd'; import { Switch } from 'antd';
import { import {
createInputStatus, createInputStatus,
@ -11,13 +11,12 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
import { RESET_TIMEOUT, postConfigUpdateToAPI } from '../../utils/config-constants'; import { RESET_TIMEOUT, postConfigUpdateToAPI } from '../../utils/config-constants';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
interface ToggleSwitchProps { export type ToggleSwitchProps = {
fieldName: string; fieldName: string;
apiPath?: string; apiPath?: string;
@ -29,8 +28,20 @@ interface ToggleSwitchProps {
tip?: string; tip?: string;
useSubmit?: boolean; useSubmit?: boolean;
onChange?: (arg: boolean) => void; onChange?: (arg: boolean) => void;
} };
export default function ToggleSwitch(props: ToggleSwitchProps) {
export const ToggleSwitch: FC<ToggleSwitchProps> = ({
apiPath,
checked,
reversed = false,
configPath = '',
disabled = false,
fieldName,
label,
tip,
useSubmit,
onChange,
}) => {
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
let resetTimer = null; let resetTimer = null;
@ -38,19 +49,6 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { setFieldInConfigState } = serverStatusData || {}; const { setFieldInConfigState } = serverStatusData || {};
const {
apiPath,
checked,
reversed = false,
configPath = '',
disabled = false,
fieldName,
label,
tip,
useSubmit,
onChange,
} = props;
const resetStates = () => { const resetStates = () => {
setSubmitStatus(null); setSubmitStatus(null);
clearTimeout(resetTimer); clearTimeout(resetTimer);
@ -107,7 +105,8 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
</div> </div>
</div> </div>
); );
} };
export default ToggleSwitch;
ToggleSwitch.defaultProps = { ToggleSwitch.defaultProps = {
apiPath: '', apiPath: '',

View file

@ -1,4 +1,4 @@
import React, { useContext, useState, useEffect } from 'react'; import React, { useContext, useState, useEffect, FC } from 'react';
import { Typography, Slider } from 'antd'; import { Typography, Slider } from 'antd';
import { ServerStatusContext } from '../../utils/server-status-context'; import { ServerStatusContext } from '../../utils/server-status-context';
import { AlertMessageContext } from '../../utils/alert-message-context'; import { AlertMessageContext } from '../../utils/alert-message-context';
@ -14,7 +14,7 @@ import {
STATUS_PROCESSING, STATUS_PROCESSING,
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../utils/input-statuses'; } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import { FormStatusIndicator } from './FormStatusIndicator';
const { Title } = Typography; const { Title } = Typography;
@ -34,7 +34,7 @@ const SLIDER_COMMENTS = {
4: 'Highest latency, highest error tolerance', 4: 'Highest latency, highest error tolerance',
}; };
export default function VideoLatency() { export const VideoLatency: FC = () => {
const [submitStatus, setSubmitStatus] = useState<StatusState>(null); const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [selectedOption, setSelectedOption] = useState(null); const [selectedOption, setSelectedOption] = useState(null);
@ -130,4 +130,4 @@ export default function VideoLatency() {
</div> </div>
</div> </div>
); );
} };

View file

@ -1,10 +1,10 @@
// This content populates the video variant modal, which is spawned from the variants table. This relies on the `dataState` prop fed in by the table. // This content populates the video variant modal, which is spawned from the variants table. This relies on the `dataState` prop fed in by the table.
import React from 'react'; import React, { FC } from 'react';
import { Popconfirm, Row, Col, Slider, Collapse, Typography } from 'antd'; import { Popconfirm, Row, Col, Slider, Collapse, Typography } from 'antd';
import { ExclamationCircleFilled } from '@ant-design/icons'; import { ExclamationCircleFilled } from '@ant-design/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import { FieldUpdaterFunc, VideoVariant, UpdateArgs } from '../../types/config-section'; import { FieldUpdaterFunc, VideoVariant, UpdateArgs } from '../../types/config-section';
import TextField from './form-textfield'; import { TextField } from './TextField';
import { import {
DEFAULT_VARIANT_STATE, DEFAULT_VARIANT_STATE,
VIDEO_VARIANT_SETTING_DEFAULTS, VIDEO_VARIANT_SETTING_DEFAULTS,
@ -17,19 +17,19 @@ import {
FRAMERATE_DEFAULTS, FRAMERATE_DEFAULTS,
FRAMERATE_TOOLTIPS, FRAMERATE_TOOLTIPS,
} from '../../utils/config-constants'; } from '../../utils/config-constants';
import ToggleSwitch from './form-toggleswitch'; import { ToggleSwitch } from './ToggleSwitch';
const { Panel } = Collapse; const { Panel } = Collapse;
interface VideoVariantFormProps { export type VideoVariantFormProps = {
dataState: VideoVariant; dataState: VideoVariant;
onUpdateField: FieldUpdaterFunc; onUpdateField: FieldUpdaterFunc;
} };
export default function VideoVariantForm({ export const VideoVariantForm: FC<VideoVariantFormProps> = ({
dataState = DEFAULT_VARIANT_STATE, dataState = DEFAULT_VARIANT_STATE,
onUpdateField, onUpdateField,
}: VideoVariantFormProps) { }) => {
const videoPassthroughEnabled = dataState.videoPassthrough; const videoPassthroughEnabled = dataState.videoPassthrough;
const handleFramerateChange = (value: number) => { const handleFramerateChange = (value: number) => {
@ -314,4 +314,4 @@ export default function VideoVariantForm({
</Collapse> </Collapse>
</div> </div>
); );
} };

View file

@ -1,13 +1,13 @@
import { Button, Typography } from 'antd'; import { Button, Typography } from 'antd';
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import TextField, { TEXTFIELD_TYPE_TEXTAREA } from '../form-textfield'; import { TextField, TEXTFIELD_TYPE_TEXTAREA } from '../TextField';
import { import {
postConfigUpdateToAPI, postConfigUpdateToAPI,
RESET_TIMEOUT, RESET_TIMEOUT,
BROWSER_PUSH_CONFIG_FIELDS, BROWSER_PUSH_CONFIG_FIELDS,
} from '../../../utils/config-constants'; } from '../../../utils/config-constants';
import ToggleSwitch from '../form-toggleswitch'; import { ToggleSwitch } from '../ToggleSwitch';
import { import {
createInputStatus, createInputStatus,
StatusState, StatusState,
@ -15,11 +15,11 @@ import {
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../../utils/input-statuses'; } from '../../../utils/input-statuses';
import { UpdateArgs } from '../../../types/config-section'; import { UpdateArgs } from '../../../types/config-section';
import FormStatusIndicator from '../form-status-indicator'; import { FormStatusIndicator } from '../FormStatusIndicator';
const { Title } = Typography; const { Title } = Typography;
export default function ConfigNotify() { export const ConfigNotify = () => {
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { notifications } = serverConfig || {}; const { notifications } = serverConfig || {};
@ -126,4 +126,5 @@ export default function ConfigNotify() {
<FormStatusIndicator status={submitStatus} /> <FormStatusIndicator status={submitStatus} />
</> </>
); );
} };
export default ConfigNotify;

View file

@ -1,14 +1,14 @@
import { Button, Typography } from 'antd'; import { Button, Typography } from 'antd';
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import TextField from '../form-textfield'; import { TextField } from '../TextField';
import FormStatusIndicator from '../form-status-indicator'; import { FormStatusIndicator } from '../FormStatusIndicator';
import { import {
postConfigUpdateToAPI, postConfigUpdateToAPI,
RESET_TIMEOUT, RESET_TIMEOUT,
DISCORD_CONFIG_FIELDS, DISCORD_CONFIG_FIELDS,
} from '../../../utils/config-constants'; } from '../../../utils/config-constants';
import ToggleSwitch from '../form-toggleswitch'; import { ToggleSwitch } from '../ToggleSwitch';
import { import {
createInputStatus, createInputStatus,
StatusState, StatusState,
@ -19,7 +19,7 @@ import { UpdateArgs } from '../../../types/config-section';
const { Title } = Typography; const { Title } = Typography;
export default function ConfigNotify() { export const ConfigNotify = () => {
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { notifications } = serverConfig || {}; const { notifications } = serverConfig || {};
@ -150,4 +150,5 @@ export default function ConfigNotify() {
<FormStatusIndicator status={submitStatus} /> <FormStatusIndicator status={submitStatus} />
</> </>
); );
} };
export default ConfigNotify;

View file

@ -5,7 +5,7 @@ import { ServerStatusContext } from '../../../utils/server-status-context';
const { Title } = Typography; const { Title } = Typography;
export default function ConfigNotify() { export const ConfigNotify = () => {
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig } = serverStatusData || {}; const { serverConfig } = serverStatusData || {};
const { federation } = serverConfig || {}; const { federation } = serverConfig || {};
@ -48,4 +48,5 @@ export default function ConfigNotify() {
</Link> </Link>
</> </>
); );
} };
export default ConfigNotify;

View file

@ -1,14 +1,14 @@
import { Button, Typography } from 'antd'; import { Button, Typography } from 'antd';
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import TextField, { TEXTFIELD_TYPE_PASSWORD } from '../form-textfield'; import { TextField, TEXTFIELD_TYPE_PASSWORD } from '../TextField';
import FormStatusIndicator from '../form-status-indicator'; import { FormStatusIndicator } from '../FormStatusIndicator';
import { import {
postConfigUpdateToAPI, postConfigUpdateToAPI,
RESET_TIMEOUT, RESET_TIMEOUT,
TWITTER_CONFIG_FIELDS, TWITTER_CONFIG_FIELDS,
} from '../../../utils/config-constants'; } from '../../../utils/config-constants';
import ToggleSwitch from '../form-toggleswitch'; import { ToggleSwitch } from '../ToggleSwitch';
import { import {
createInputStatus, createInputStatus,
StatusState, StatusState,
@ -16,11 +16,11 @@ import {
STATUS_SUCCESS, STATUS_SUCCESS,
} from '../../../utils/input-statuses'; } from '../../../utils/input-statuses';
import { UpdateArgs } from '../../../types/config-section'; import { UpdateArgs } from '../../../types/config-section';
import { TEXTFIELD_TYPE_TEXT } from '../form-textfield-with-submit'; import { TEXTFIELD_TYPE_TEXT } from '../TextFieldWithSubmit';
const { Title } = Typography; const { Title } = Typography;
export default function ConfigNotify() { export const ConfigNotify = () => {
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { notifications } = serverConfig || {}; const { notifications } = serverConfig || {};
@ -222,4 +222,5 @@ export default function ConfigNotify() {
<FormStatusIndicator status={submitStatus} /> <FormStatusIndicator status={submitStatus} />
</> </>
); );
} };
export default ConfigNotify;

View file

@ -0,0 +1,15 @@
import { AppProps } from 'next/app';
import { FC } from 'react';
import ServerStatusProvider from '../../utils/server-status-context';
import AlertMessageProvider from '../../utils/alert-message-context';
import { MainLayout } from '../MainLayout';
export const AdminLayout: FC<AppProps> = ({ Component, pageProps }) => (
<ServerStatusProvider>
<AlertMessageProvider>
<MainLayout>
<Component {...pageProps} />
</MainLayout>
</AlertMessageProvider>
</ServerStatusProvider>
);

View file

@ -1,20 +1,21 @@
import { Layout } from 'antd'; import { Layout } from 'antd';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import Head from 'next/head'; import Head from 'next/head';
import { useEffect, useRef } from 'react'; import { FC, useEffect, useRef } from 'react';
import { import {
ClientConfigStore, ClientConfigStore,
isChatAvailableSelector, isChatAvailableSelector,
clientConfigStateAtom, clientConfigStateAtom,
fatalErrorStateAtom, fatalErrorStateAtom,
} from '../stores/ClientConfigStore'; } from '../stores/ClientConfigStore';
import { Content, Header } from '../ui'; import { Content } from '../ui/Content/Content';
import { Header } from '../ui/Header/Header';
import { ClientConfig } from '../../interfaces/client-config.model'; import { ClientConfig } from '../../interfaces/client-config.model';
import { DisplayableError } from '../../types/displayable-error'; import { DisplayableError } from '../../types/displayable-error';
import FatalErrorStateModal from '../modals/FatalErrorStateModal/FatalErrorStateModal'; import { FatalErrorStateModal } from '../modals/FatalErrorStateModal/FatalErrorStateModal';
import setupNoLinkReferrer from '../../utils/no-link-referrer'; import setupNoLinkReferrer from '../../utils/no-link-referrer';
function Main() { export const Main: FC = () => {
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom); const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const { name, title, customStyles } = clientConfig; const { name, title, customStyles } = clientConfig;
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector); const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
@ -97,6 +98,4 @@ function Main() {
</Layout> </Layout>
</> </>
); );
} };
export default Main;

View file

@ -1,11 +1,8 @@
import { AppProps } from 'next/app'; import { AppProps } from 'next/app';
import { FC } from 'react';
function SimpleLayout({ Component, pageProps }: AppProps) { export const SimpleLayout: FC<AppProps> = ({ Component, pageProps }) => (
return ( <div>
<div> <Component {...pageProps} />
<Component {...pageProps} /> </div>
</div> );
);
}
export default SimpleLayout;

View file

@ -1,18 +0,0 @@
import { AppProps } from 'next/app';
import ServerStatusProvider from '../../utils/server-status-context';
import AlertMessageProvider from '../../utils/alert-message-context';
import MainLayout from '../main-layout';
function AdminLayout({ Component, pageProps }: AppProps) {
return (
<ServerStatusProvider>
<AlertMessageProvider>
<MainLayout>
<Component {...pageProps} />
</MainLayout>
</AlertMessageProvider>
</ServerStatusProvider>
);
}
export default AdminLayout;

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import AuthModal from './AuthModal'; import { AuthModal } from './AuthModal';
const Example = () => ( const Example = () => (
<div> <div>

View file

@ -1,12 +1,13 @@
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import IndieAuthModal from '../IndieAuthModal/IndieAuthModal'; import { FC } from 'react';
import FediAuthModal from '../FediAuthModal/FediAuthModal'; import { IndieAuthModal } from '../IndieAuthModal/IndieAuthModal';
import { FediAuthModal } from '../FediAuthModal/FediAuthModal';
import FediverseIcon from '../../../assets/images/fediverse-black.png'; import FediverseIcon from '../../../assets/images/fediverse-black.png';
import IndieAuthIcon from '../../../assets/images/indieauth.png'; import IndieAuthIcon from '../../../assets/images/indieauth.png';
import s from './AuthModal.module.scss'; import styles from './AuthModal.module.scss';
import { import {
chatDisplayNameAtom, chatDisplayNameAtom,
chatAuthenticatedAtom, chatAuthenticatedAtom,
@ -15,10 +16,7 @@ import {
const { TabPane } = Tabs; const { TabPane } = Tabs;
/* eslint-disable @typescript-eslint/no-unused-vars */ export const AuthModal: FC = () => {
interface Props {}
export default function AuthModal(props: Props) {
const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom); const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom);
const authenticated = useRecoilValue<boolean>(chatAuthenticatedAtom); const authenticated = useRecoilValue<boolean>(chatAuthenticatedAtom);
const accessToken = useRecoilValue<string>(accessTokenAtom); const accessToken = useRecoilValue<string>(accessTokenAtom);
@ -34,8 +32,8 @@ export default function AuthModal(props: Props) {
> >
<TabPane <TabPane
tab={ tab={
<span className={s.tabContent}> <span className={styles.tabContent}>
<img className={s.icon} src={IndieAuthIcon.src} alt="IndieAuth" /> <img className={styles.icon} src={IndieAuthIcon.src} alt="IndieAuth" />
IndieAuth IndieAuth
</span> </span>
} }
@ -49,8 +47,8 @@ export default function AuthModal(props: Props) {
</TabPane> </TabPane>
<TabPane <TabPane
tab={ tab={
<span className={s.tabContent}> <span className={styles.tabContent}>
<img className={s.icon} src={FediverseIcon.src} alt="Fediverse auth" /> <img className={styles.icon} src={FediverseIcon.src} alt="Fediverse auth" />
FediAuth FediAuth
</span> </span>
} }
@ -61,4 +59,4 @@ export default function AuthModal(props: Props) {
</Tabs> </Tabs>
</div> </div>
); );
} };

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import BrowserNotifyModal from './BrowserNotifyModal'; import { BrowserNotifyModal } from './BrowserNotifyModal';
import BrowserNotifyModalMock from '../../../stories/assets/mocks/notify-modal.png'; import BrowserNotifyModalMock from '../../../stories/assets/mocks/notify-modal.png';
const Example = () => ( const Example = () => (

View file

@ -1,69 +1,64 @@
import { Row, Col, Spin, Typography, Button } from 'antd'; import { Row, Col, Spin, Typography, Button } from 'antd';
import React, { useState } from 'react'; import React, { FC, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { accessTokenAtom, clientConfigStateAtom } from '../../stores/ClientConfigStore'; import { accessTokenAtom, clientConfigStateAtom } from '../../stores/ClientConfigStore';
import { import {
registerWebPushNotifications, registerWebPushNotifications,
saveNotificationRegistration, saveNotificationRegistration,
} from '../../../services/notifications-service'; } from '../../../services/notifications-service';
import s from './BrowserNotifyModal.module.scss'; import styles from './BrowserNotifyModal.module.scss';
import isPushNotificationSupported from '../../../utils/browserPushNotifications'; import isPushNotificationSupported from '../../../utils/browserPushNotifications';
const { Title } = Typography; const { Title } = Typography;
function NotificationsNotSupported() { const NotificationsNotSupported = () => (
return <div>Browser notifications are not supported in your browser.</div>; <div>Browser notifications are not supported in your browser.</div>
} );
function NotificationsEnabled() { const NotificationsEnabled = () => <div>Notifications enabled</div>;
return <div>Notifications enabled</div>;
}
interface PermissionPopupPreviewProps { export type PermissionPopupPreviewProps = {
start: () => void; start: () => void;
} };
function PermissionPopupPreview(props: PermissionPopupPreviewProps) {
const { start } = props;
return ( const PermissionPopupPreview: FC<PermissionPopupPreviewProps> = ({ start }) => (
<div id="browser-push-preview-box" className={s.pushPreview}> <div id="browser-push-preview-box" className={styles.pushPreview}>
<div className={s.inner}> <div className={styles.inner}>
<div className={s.title}>{window.location.toString()} wants to</div> <div className={styles.title}>{window.location.toString()} wants to</div>
<div className={s.permissionLine}> <div className={styles.permissionLine}>
<svg <svg
width="16" width="16"
height="16" height="16"
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M14 12.3333V13H2V12.3333L3.33333 11V7C3.33333 4.93333 4.68667 3.11333 6.66667 2.52667C6.66667 2.46 6.66667 2.4 6.66667 2.33333C6.66667 1.97971 6.80714 1.64057 7.05719 1.39052C7.30724 1.14048 7.64638 1 8 1C8.35362 1 8.69276 1.14048 8.94281 1.39052C9.19286 1.64057 9.33333 1.97971 9.33333 2.33333C9.33333 2.4 9.33333 2.46 9.33333 2.52667C11.3133 3.11333 12.6667 4.93333 12.6667 7V11L14 12.3333ZM9.33333 13.6667C9.33333 14.0203 9.19286 14.3594 8.94281 14.6095C8.69276 14.8595 8.35362 15 8 15C7.64638 15 7.30724 14.8595 7.05719 14.6095C6.80714 14.3594 6.66667 14.0203 6.66667 13.6667" d="M14 12.3333V13H2V12.3333L3.33333 11V7C3.33333 4.93333 4.68667 3.11333 6.66667 2.52667C6.66667 2.46 6.66667 2.4 6.66667 2.33333C6.66667 1.97971 6.80714 1.64057 7.05719 1.39052C7.30724 1.14048 7.64638 1 8 1C8.35362 1 8.69276 1.14048 8.94281 1.39052C9.19286 1.64057 9.33333 1.97971 9.33333 2.33333C9.33333 2.4 9.33333 2.46 9.33333 2.52667C11.3133 3.11333 12.6667 4.93333 12.6667 7V11L14 12.3333ZM9.33333 13.6667C9.33333 14.0203 9.19286 14.3594 8.94281 14.6095C8.69276 14.8595 8.35362 15 8 15C7.64638 15 7.30724 14.8595 7.05719 14.6095C6.80714 14.3594 6.66667 14.0203 6.66667 13.6667"
fill="#676670" fill="#676670"
/> />
</svg> </svg>
Show notifications Show notifications
</div> </div>
<div className={s.buttonRow}> <div className={styles.buttonRow}>
<Button <Button
type="primary" type="primary"
className={s.allow} className={styles.allow}
onClick={() => { onClick={() => {
start(); start();
}} }}
> >
Allow Allow
</Button> </Button>
<button type="button" className={s.disabled}> <button type="button" className={styles.disabled}>
Block Block
</button> </button>
</div>
</div> </div>
</div> </div>
); </div>
} );
export default function BrowserNotifyModal() { export const BrowserNotifyModal = () => {
const [error, setError] = useState<string>(null); const [error, setError] = useState<string>(null);
const accessToken = useRecoilValue(accessTokenAtom); const accessToken = useRecoilValue(accessTokenAtom);
const config = useRecoilValue(clientConfigStateAtom); const config = useRecoilValue(clientConfigStateAtom);
@ -120,4 +115,4 @@ export default function BrowserNotifyModal() {
</Row> </Row>
</Spin> </Spin>
); );
} };

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react'; import { ComponentStory, ComponentMeta } from '@storybook/react';
import FatalErrorStateModal from './FatalErrorStateModal'; import { FatalErrorStateModal } from './FatalErrorStateModal';
export default { export default {
title: 'owncast/Modals/Global error state', title: 'owncast/Modals/Global error state',

View file

@ -1,25 +1,22 @@
import { Modal } from 'antd'; import { Modal } from 'antd';
import { FC } from 'react';
interface Props { export type FatalErrorStateModalProps = {
title: string; title: string;
message: string; message: string;
} };
export default function FatalErrorStateModal(props: Props) { export const FatalErrorStateModal: FC<FatalErrorStateModalProps> = ({ title, message }) => (
const { title, message } = props; <Modal
title={title}
return ( visible
<Modal footer={null}
title={title} closable={false}
visible keyboard={false}
footer={null} width={900}
closable={false} centered
keyboard={false} className="modal"
width={900} >
centered <p style={{ fontSize: '1.3rem' }}>{message}</p>
className="modal" </Modal>
> );
<p style={{ fontSize: '1.3rem' }}>{message}</p>
</Modal>
);
}

Some files were not shown because too many files have changed in this diff Show more