mirror of
https://github.com/owncast/owncast.git
synced 2024-11-21 20:28:15 +03:00
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:
parent
ee333ef10a
commit
d1f3fffe2f
178 changed files with 1258 additions and 1227 deletions
|
@ -22,7 +22,9 @@ module.exports = {
|
|||
ignorePatterns: ['!./storybook/**'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'react/prop-types': 0,
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/require-default-props': 'off',
|
||||
'react/jsx-filename-extension': [
|
||||
1,
|
||||
{
|
||||
|
@ -31,7 +33,13 @@ module.exports = {
|
|||
],
|
||||
'react/jsx-props-no-spreading': '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',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Meta } from '@storybook/addon-docs';
|
||||
import { Typography } from 'antd';
|
||||
import UserChatMessage from '../../components/chat/ChatUserMessage';
|
||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||
|
||||
<Meta title="Owncast/Documentation/Chat" />
|
||||
|
|
22
web/components/.eslintrc.js
Normal file
22
web/components/.eslintrc.js
Normal 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',
|
||||
},
|
||||
};
|
|
@ -1,15 +1,17 @@
|
|||
import { Modal, Button } from 'antd';
|
||||
import { ExclamationCircleFilled, QuestionCircleFilled, StopTwoTone } from '@ant-design/icons';
|
||||
import { USER_ENABLED_TOGGLE, fetchData } from '../../../utils/apis';
|
||||
import { User } from '../../../types/chat';
|
||||
import { FC } from 'react';
|
||||
import { USER_ENABLED_TOGGLE, fetchData } from '../utils/apis';
|
||||
import { User } from '../types/chat';
|
||||
|
||||
interface BanUserButtonProps {
|
||||
export type BanUserButtonProps = {
|
||||
user: User;
|
||||
isEnabled: Boolean; // = this user's current status
|
||||
label?: string;
|
||||
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> {
|
||||
const data = {
|
||||
userId: id,
|
||||
|
@ -78,7 +80,7 @@ export default function BanUserButton({ user, isEnabled, label, onClick }: BanUs
|
|||
{label || actionString}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
BanUserButton.defaultProps = {
|
||||
label: '',
|
||||
onClick: null,
|
|
@ -1,7 +1,7 @@
|
|||
import { Table, Button } from 'antd';
|
||||
import format from 'date-fns/format';
|
||||
import { SortOrder } from 'antd/lib/table/interface';
|
||||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { StopTwoTone } from '@ant-design/icons';
|
||||
import { User } from '../types/chat';
|
||||
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 = [
|
||||
{
|
||||
title: 'IP Address',
|
||||
|
@ -68,8 +72,4 @@ export default function BannedIPsTable({ data }: UserTableProps) {
|
|||
rowKey="ipAddress"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface UserTableProps {
|
||||
data: User[];
|
||||
}
|
||||
};
|
|
@ -2,6 +2,7 @@ import ChartJs from 'chart.js/auto';
|
|||
import Chartkick from 'chartkick';
|
||||
import format from 'date-fns/format';
|
||||
import { LineChart } from 'react-chartkick';
|
||||
import { FC } from 'react';
|
||||
|
||||
// from https://github.com/ankane/chartkick.js/blob/master/chart.js/chart.esm.js
|
||||
Chartkick.use(ChartJs);
|
||||
|
@ -11,7 +12,7 @@ interface TimedValue {
|
|||
value: number;
|
||||
}
|
||||
|
||||
interface ChartProps {
|
||||
export type ChartProps = {
|
||||
data?: TimedValue[];
|
||||
title?: string;
|
||||
color: string;
|
||||
|
@ -19,7 +20,7 @@ interface ChartProps {
|
|||
yFlipped?: boolean;
|
||||
yLogarithmic?: boolean;
|
||||
dataCollections?: any[];
|
||||
}
|
||||
};
|
||||
|
||||
function createGraphDataset(dataArray) {
|
||||
const dataValues = {};
|
||||
|
@ -31,7 +32,7 @@ function createGraphDataset(dataArray) {
|
|||
return dataValues;
|
||||
}
|
||||
|
||||
export default function Chart({
|
||||
export const Chart: FC<ChartProps> = ({
|
||||
data,
|
||||
title,
|
||||
color,
|
||||
|
@ -39,7 +40,7 @@ export default function Chart({
|
|||
dataCollections,
|
||||
yFlipped,
|
||||
yLogarithmic,
|
||||
}: ChartProps) {
|
||||
}) => {
|
||||
const renderData = [];
|
||||
|
||||
if (data && data.length > 0) {
|
||||
|
@ -87,7 +88,7 @@ export default function Chart({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Chart.defaultProps = {
|
||||
dataCollections: [],
|
|
@ -3,12 +3,17 @@ import { FilterDropdownProps, SortOrder } from 'antd/lib/table/interface';
|
|||
import { ColumnsType } from 'antd/es/table';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { FC } from 'react';
|
||||
import { Client } from '../types/chat';
|
||||
import UserPopover from './user-popover';
|
||||
import BanUserButton from './other/ban-user-button/ban-user-button';
|
||||
import { UserPopover } from './UserPopover';
|
||||
import { BanUserButton } from './BanUserButton';
|
||||
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> = [
|
||||
{
|
||||
title: 'Display Name',
|
||||
|
@ -91,8 +96,4 @@ export default function ClientTable({ data }: ClientTableProps) {
|
|||
rowKey="id"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ClientTableProps {
|
||||
data: Client[];
|
||||
}
|
||||
};
|
|
@ -1,7 +1,11 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { FC } from 'react';
|
||||
|
||||
export function Color(props) {
|
||||
const { color } = props;
|
||||
export type ColorProps = {
|
||||
color: any; // TODO specify better type
|
||||
};
|
||||
|
||||
export const Color: FC<ColorProps> = ({ color }) => {
|
||||
const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(`--${color}`);
|
||||
|
||||
const containerStyle = {
|
||||
|
@ -48,7 +52,7 @@ export function Color(props) {
|
|||
<figcaption style={colorDescriptionStyle}>{color}</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Color.propTypes = {
|
||||
color: PropTypes.string.isRequired,
|
||||
|
@ -61,7 +65,7 @@ const rowStyle = {
|
|||
alignItems: 'center',
|
||||
};
|
||||
|
||||
export function ColorRow(props) {
|
||||
export const ColorRow = props => {
|
||||
const { colors } = props;
|
||||
|
||||
return (
|
||||
|
@ -71,7 +75,7 @@ export function ColorRow(props) {
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ColorRow.propTypes = {
|
||||
colors: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { FC, useState } from 'react';
|
||||
|
||||
import { Button, Space, Input, Modal } from 'antd';
|
||||
import { STATUS_ERROR, STATUS_SUCCESS } from '../utils/input-statuses';
|
||||
|
@ -6,12 +6,12 @@ import { fetchData, FEDERATION_MESSAGE_SEND } from '../utils/apis';
|
|||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface ComposeFederatedPostProps {
|
||||
export type ComposeFederatedPostProps = {
|
||||
visible: boolean;
|
||||
handleClose: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
export default function ComposeFederatedPost({ visible, handleClose }: ComposeFederatedPostProps) {
|
||||
export const ComposeFederatedPost: FC<ComposeFederatedPostProps> = ({ visible, handleClose }) => {
|
||||
const [content, setContent] = useState('');
|
||||
const [postPending, setPostPending] = useState(false);
|
||||
const [postSuccessState, setPostSuccessState] = useState(null);
|
||||
|
@ -79,4 +79,4 @@ export default function ComposeFederatedPost({ visible, handleClose }: ComposeFe
|
|||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,6 +1,11 @@
|
|||
export function ImageAsset(props: ImageAssetProps) {
|
||||
const { name, src } = props;
|
||||
import { FC } from 'react';
|
||||
|
||||
export type ImageAssetProps = {
|
||||
name: string;
|
||||
src: string;
|
||||
};
|
||||
|
||||
export const ImageAsset: FC<ImageAssetProps> = ({ name, src }) => {
|
||||
const containerStyle = {
|
||||
borderRadius: '20px',
|
||||
width: '12vw',
|
||||
|
@ -38,12 +43,7 @@ export function ImageAsset(props: ImageAssetProps) {
|
|||
</a>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
interface ImageAssetProps {
|
||||
name: string;
|
||||
src: string;
|
||||
}
|
||||
};
|
||||
|
||||
const rowStyle = {
|
||||
display: 'flex',
|
||||
|
@ -53,7 +53,7 @@ const rowStyle = {
|
|||
alignItems: 'center',
|
||||
};
|
||||
|
||||
export function ImageRow(props: ImageRowProps) {
|
||||
export const ImageRow = (props: ImageRowProps) => {
|
||||
const { images } = props;
|
||||
|
||||
return (
|
||||
|
@ -63,7 +63,7 @@ export function ImageRow(props: ImageRowProps) {
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface ImageRowProps {
|
||||
images: ImageAssetProps[];
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { FC } from 'react';
|
||||
|
||||
interface InfoTipProps {
|
||||
export type InfoTipProps = {
|
||||
tip: string | null;
|
||||
}
|
||||
};
|
||||
|
||||
export default function InfoTip({ tip }: InfoTipProps) {
|
||||
export const InfoTip: FC<InfoTipProps> = ({ tip }) => {
|
||||
if (tip === '' || tip === null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -17,4 +18,4 @@ export default function InfoTip({ tip }: InfoTipProps) {
|
|||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,8 +1,14 @@
|
|||
import { Table, Typography } from 'antd';
|
||||
import { FC } from 'react';
|
||||
|
||||
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 = [
|
||||
{
|
||||
title: 'Name',
|
||||
|
@ -22,9 +28,4 @@ export default function KeyValueTable({ title, data }: KeyValueTableProps) {
|
|||
<Table pagination={false} columns={columns} dataSource={data} rowKey="name" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface KeyValueTableProps {
|
||||
title: string;
|
||||
data: any;
|
||||
}
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { Table, Tag, Typography } from 'antd';
|
||||
import Linkify from 'react-linkify';
|
||||
import { SortOrder } from 'antd/lib/table/interface';
|
||||
|
@ -22,12 +22,12 @@ function renderMessage(text) {
|
|||
return <Linkify>{text}</Linkify>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
export type LogTableProps = {
|
||||
logs: object[];
|
||||
pageSize: number;
|
||||
}
|
||||
};
|
||||
|
||||
export default function LogTable({ logs, pageSize }: Props) {
|
||||
export const LogTable: FC<LogTableProps> = ({ logs, pageSize }) => {
|
||||
if (!logs?.length) {
|
||||
return null;
|
||||
}
|
||||
|
@ -85,4 +85,4 @@ export default function LogTable({ logs, pageSize }: Props) {
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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 Link from 'next/link';
|
||||
import Head from 'next/head';
|
||||
|
@ -21,19 +21,20 @@ import classNames from 'classnames';
|
|||
import { upgradeVersionAvailable } from '../utils/apis';
|
||||
import { parseSecondsToDurationString } from '../utils/format';
|
||||
|
||||
import { OwncastLogo } from './common';
|
||||
import { OwncastLogo } from './common/OwncastLogo/OwncastLogo';
|
||||
import { ServerStatusContext } from '../utils/server-status-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 ComposeFederatedPost from './compose-federated-post';
|
||||
import { ComposeFederatedPost } from './ComposeFederatedPost';
|
||||
import { UpdateArgs } from '../types/config-section';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function MainLayout(props) {
|
||||
const { children } = props;
|
||||
export type MainLayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
|
||||
const context = useContext(ServerStatusContext);
|
||||
const { serverConfig, online, broadcaster, versionNumber } = context || {};
|
||||
const { instanceDetails, chatDisabled, federation } = serverConfig;
|
||||
|
@ -287,7 +288,7 @@ export default function MainLayout(props) {
|
|||
/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
MainLayout.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
|
@ -1,5 +1,5 @@
|
|||
// 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 {
|
||||
EyeOutlined,
|
||||
|
@ -12,17 +12,17 @@ import { MessageType } from '../types/chat';
|
|||
import { OUTCOME_TIMEOUT } from '../pages/admin/chat/messages';
|
||||
import { isEmptyObject } from '../utils/format';
|
||||
|
||||
interface MessageToggleProps {
|
||||
export type MessageToggleProps = {
|
||||
isVisible: boolean;
|
||||
message: MessageType;
|
||||
setMessage: (message: MessageType) => void;
|
||||
}
|
||||
};
|
||||
|
||||
export default function MessageVisiblityToggle({
|
||||
export const MessageVisiblityToggle: FC<MessageToggleProps> = ({
|
||||
isVisible,
|
||||
message,
|
||||
setMessage,
|
||||
}: MessageToggleProps) {
|
||||
}) => {
|
||||
if (!message || isEmptyObject(message)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -89,4 +89,4 @@ export default function MessageVisiblityToggle({
|
|||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -5,14 +5,16 @@ import {
|
|||
StopTwoTone,
|
||||
SafetyCertificateTwoTone,
|
||||
} from '@ant-design/icons';
|
||||
import { FC } from 'react';
|
||||
import { USER_SET_MODERATOR, fetchData } from '../utils/apis';
|
||||
import { User } from '../types/chat';
|
||||
|
||||
interface ModeratorUserButtonProps {
|
||||
export type ModeratorUserButtonProps = {
|
||||
user: User;
|
||||
onClick?: () => void;
|
||||
}
|
||||
export default function ModeratorUserButton({ user, onClick }: ModeratorUserButtonProps) {
|
||||
};
|
||||
|
||||
export const ModeratorUserButton: FC<ModeratorUserButtonProps> = ({ user, onClick }) => {
|
||||
async function buttonClicked({ id }, setAsModerator: Boolean): Promise<Boolean> {
|
||||
const data = {
|
||||
userId: id,
|
||||
|
@ -87,7 +89,8 @@ export default function ModeratorUserButton({ user, onClick }: ModeratorUserButt
|
|||
{actionString}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ModeratorUserButton.defaultProps = {
|
||||
onClick: null,
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable camelcase */
|
||||
/* 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 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_BASE_URL = 'https://owncast.online';
|
||||
|
||||
interface Article {
|
||||
export type ArticleProps = {
|
||||
title: string;
|
||||
url: string;
|
||||
content_html: 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 dateString = format(dateObject, 'MMM dd, yyyy, HH:mm');
|
||||
return (
|
||||
|
@ -38,10 +43,10 @@ function ArticleItem({ title, url, content_html: content, date_published: date }
|
|||
</Collapse>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function NewsFeed() {
|
||||
const [feed, setFeed] = useState<Article[]>([]);
|
||||
export const NewsFeed = () => {
|
||||
const [feed, setFeed] = useState<ArticleProps[]>([]);
|
||||
const [loading, setLoading] = useState<Boolean>(true);
|
||||
|
||||
const getFeed = async () => {
|
||||
|
@ -75,4 +80,4 @@ export default function NewsFeed() {
|
|||
{noNews}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,10 +1,10 @@
|
|||
import { BookTwoTone, MessageTwoTone, PlaySquareTwoTone, ProfileTwoTone } from '@ant-design/icons';
|
||||
import { Card, Col, Row, Typography } from 'antd';
|
||||
import Link from 'next/link';
|
||||
import { useContext } from 'react';
|
||||
import LogTable from './log-table';
|
||||
import OwncastLogo from './common/Logo/Logo';
|
||||
import NewsFeed from './news-feed';
|
||||
import { FC, useContext } from 'react';
|
||||
import { LogTable } from './LogTable';
|
||||
import { OwncastLogo } from './common/OwncastLogo/OwncastLogo';
|
||||
import { NewsFeed } from './NewsFeed';
|
||||
import { ConfigDetails } from '../types/config-section';
|
||||
import { ServerStatusContext } from '../utils/server-status-context';
|
||||
|
||||
|
@ -17,12 +17,12 @@ function generateStreamURL(serverURL, rtmpServerPort) {
|
|||
return `rtmp://${serverURL.replace(/(^\w+:|^)\/\//, '')}:${rtmpServerPort}/live`;
|
||||
}
|
||||
|
||||
type OfflineProps = {
|
||||
export type OfflineProps = {
|
||||
logs: any[];
|
||||
config: ConfigDetails;
|
||||
};
|
||||
|
||||
export default function Offline({ logs = [], config }: OfflineProps) {
|
||||
export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
|
||||
const { serverConfig } = serverStatusData || {};
|
||||
|
@ -149,4 +149,5 @@ export default function Offline({ logs = [], config }: OfflineProps) {
|
|||
<LogTable logs={logs} pageSize={5} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default Offline;
|
|
@ -3,10 +3,11 @@
|
|||
// 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 { FC } from 'react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface StatisticItemProps {
|
||||
export type StatisticItemProps = {
|
||||
title?: string;
|
||||
value?: any;
|
||||
prefix?: any;
|
||||
|
@ -15,7 +16,8 @@ interface StatisticItemProps {
|
|||
progress?: boolean;
|
||||
centered?: boolean;
|
||||
formatter?: any;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
title: '',
|
||||
value: 0,
|
||||
|
@ -27,31 +29,29 @@ const defaultProps = {
|
|||
formatter: null,
|
||||
};
|
||||
|
||||
interface ContentProps {
|
||||
export type ContentProps = {
|
||||
prefix: string;
|
||||
value: any;
|
||||
suffix: string;
|
||||
title: string;
|
||||
}
|
||||
};
|
||||
|
||||
function Content({ prefix, value, suffix, title }: ContentProps) {
|
||||
return (
|
||||
const Content: FC<ContentProps> = ({ prefix, value, suffix, title }) => (
|
||||
<div>
|
||||
{prefix}
|
||||
<div>
|
||||
{prefix}
|
||||
<div>
|
||||
<Text type="secondary">{title}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">
|
||||
{value}
|
||||
{suffix || '%'}
|
||||
</Text>
|
||||
</div>
|
||||
<Text type="secondary">{title}</Text>
|
||||
</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 content = <Content prefix={prefix} value={value} suffix={suffix} title={title} />;
|
||||
|
||||
|
@ -67,15 +67,15 @@ function ProgressView({ title, value, prefix, suffix, color }: StatisticItemProp
|
|||
format={() => content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
ProgressView.defaultProps = defaultProps;
|
||||
|
||||
function StatisticView({ title, value, prefix, formatter }: StatisticItemProps) {
|
||||
return <Statistic title={title} value={value} prefix={prefix} formatter={formatter} />;
|
||||
}
|
||||
const StatisticView: FC<StatisticItemProps> = ({ title, value, prefix, formatter }) => (
|
||||
<Statistic title={title} value={value} prefix={prefix} formatter={formatter} />
|
||||
);
|
||||
StatisticView.defaultProps = defaultProps;
|
||||
|
||||
export default function StatisticItem(props: StatisticItemProps) {
|
||||
export const StatisticItem: FC<StatisticItemProps> = props => {
|
||||
const { progress, centered } = props;
|
||||
const View = progress ? ProgressView : StatisticView;
|
||||
|
||||
|
@ -88,5 +88,5 @@ export default function StatisticItem(props: StatisticItemProps) {
|
|||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
StatisticItem.defaultProps = defaultProps;
|
|
@ -1,15 +1,14 @@
|
|||
import { CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import { Alert, Button, Col, Row, Statistic, Typography } from 'antd';
|
||||
import Link from 'next/link';
|
||||
import React, { useContext } from 'react';
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { ServerStatusContext } from '../utils/server-status-context';
|
||||
|
||||
interface StreamHealthOverviewProps {
|
||||
export type StreamHealthOverviewProps = {
|
||||
showTroubleshootButton?: Boolean;
|
||||
}
|
||||
export default function StreamHealthOverview({
|
||||
showTroubleshootButton,
|
||||
}: StreamHealthOverviewProps) {
|
||||
};
|
||||
|
||||
export const StreamHealthOverview: FC<StreamHealthOverviewProps> = ({ showTroubleshootButton }) => {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { health } = serverStatusData;
|
||||
if (!health) {
|
||||
|
@ -79,7 +78,7 @@ export default function StreamHealthOverview({
|
|||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
StreamHealthOverview.defaultProps = {
|
||||
showTroubleshootButton: true,
|
|
@ -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.
|
||||
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { useState, ReactNode, FC } from 'react';
|
||||
import { Divider, Modal, Tooltip, Typography, Row, Col, Space } from 'antd';
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||
import format from 'date-fns/format';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
import BlockUserbutton from './other/ban-user-button/ban-user-button';
|
||||
import ModeratorUserbutton from './moderator-user-button';
|
||||
import { BanUserButton } from './BanUserButton';
|
||||
import { ModeratorUserButton } from './ModeratorUserButton';
|
||||
|
||||
import { User, UserConnectionInfo } from '../types/chat';
|
||||
import { formatDisplayDate } from './user-table';
|
||||
import { formatDisplayDate } from './UserTable';
|
||||
import { formatUAstring } from '../utils/format';
|
||||
|
||||
interface UserPopoverProps {
|
||||
export type UserPopoverProps = {
|
||||
user: User;
|
||||
connectionInfo?: UserConnectionInfo | null;
|
||||
children: ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
export default function UserPopover({ user, connectionInfo, children }: UserPopoverProps) {
|
||||
export const UserPopover: FC<UserPopoverProps> = ({ user, connectionInfo, children }) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const handleShowModal = () => {
|
||||
setIsModalVisible(true);
|
||||
|
@ -123,7 +123,7 @@ export default function UserPopover({ user, connectionInfo, children }: UserPopo
|
|||
This user was banned on <code>{formatDisplayDate(disabledAt)}</code>.
|
||||
<br />
|
||||
<br />
|
||||
<BlockUserbutton
|
||||
<BanUserButton
|
||||
label="Unban this user"
|
||||
user={user}
|
||||
isEnabled={false}
|
||||
|
@ -131,20 +131,20 @@ export default function UserPopover({ user, connectionInfo, children }: UserPopo
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<BlockUserbutton
|
||||
<BanUserButton
|
||||
label="Ban this user"
|
||||
user={user}
|
||||
isEnabled
|
||||
onClick={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
<ModeratorUserbutton user={user} onClick={handleCloseModal} />
|
||||
<ModeratorUserButton user={user} onClick={handleCloseModal} />
|
||||
</Space>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
UserPopover.defaultProps = {
|
||||
connectionInfo: null,
|
|
@ -1,14 +1,20 @@
|
|||
import { Table } from 'antd';
|
||||
import format from 'date-fns/format';
|
||||
import { SortOrder } from 'antd/lib/table/interface';
|
||||
import { FC } from 'react';
|
||||
import { User } from '../types/chat';
|
||||
import UserPopover from './user-popover';
|
||||
import BanUserButton from './other/ban-user-button/ban-user-button';
|
||||
import { UserPopover } from './UserPopover';
|
||||
import { BanUserButton } from './BanUserButton';
|
||||
|
||||
export function formatDisplayDate(date: string | Date) {
|
||||
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 = [
|
||||
{
|
||||
title: 'Last Known Display Name',
|
||||
|
@ -57,8 +63,4 @@ export default function UserTable({ data }: UserTableProps) {
|
|||
rowKey="id"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface UserTableProps {
|
||||
data: User[];
|
||||
}
|
||||
};
|
|
@ -2,13 +2,19 @@ import { Table } from 'antd';
|
|||
import format from 'date-fns/format';
|
||||
import { SortOrder } from 'antd/lib/table/interface';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { FC } from 'react';
|
||||
import { User } from '../types/chat';
|
||||
import { formatUAstring } from '../utils/format';
|
||||
|
||||
export function formatDisplayDate(date: string | Date) {
|
||||
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 = [
|
||||
{
|
||||
title: 'User Agent',
|
||||
|
@ -43,8 +49,4 @@ export default function ViewerTable({ data }: ViewerTableProps) {
|
|||
rowKey="id"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ViewerTableProps {
|
||||
data: User[];
|
||||
}
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ActionButton from './ActionButton';
|
||||
import { ActionButton } from './ActionButton';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Components/Action Buttons/Single button',
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
import { Button } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import Modal from '../../ui/Modal/Modal';
|
||||
import { FC, useState } from 'react';
|
||||
import { Modal } from '../../ui/Modal/Modal';
|
||||
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;
|
||||
primary?: boolean;
|
||||
}
|
||||
ActionButton.defaultProps = {
|
||||
primary: true,
|
||||
};
|
||||
|
||||
export default function ActionButton({
|
||||
export const ActionButton: FC<ActionButtonProps> = ({
|
||||
action: { url, title, description, icon, color, openExternally },
|
||||
primary = false,
|
||||
}: Props) {
|
||||
primary = true,
|
||||
}) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const buttonClicked = () => {
|
||||
const onButtonClicked = () => {
|
||||
if (openExternally) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
|
@ -30,11 +27,11 @@ export default function ActionButton({
|
|||
<>
|
||||
<Button
|
||||
type={primary ? 'primary' : 'default'}
|
||||
className={`${s.button}`}
|
||||
onClick={buttonClicked}
|
||||
className={`${styles.button}`}
|
||||
onClick={onButtonClicked}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
<img src={icon} className={`${s.icon}`} alt={description} />
|
||||
<img src={icon} className={`${styles.icon}`} alt={description} />
|
||||
{title}
|
||||
</Button>
|
||||
<Modal
|
||||
|
@ -46,4 +43,4 @@ export default function ActionButton({
|
|||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ActionButtonRow from './ActionButtonRow';
|
||||
import ActionButton from '../ActionButton/ActionButton';
|
||||
import { ActionButtonRow } from './ActionButtonRow';
|
||||
import { ActionButton } from '../ActionButton/ActionButton';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Components/Action Buttons/Buttons Row',
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import React from 'react';
|
||||
import s from './ActionButtonRow.module.scss';
|
||||
import { FC, ReactNode } from 'react';
|
||||
import styles from './ActionButtonRow.module.scss';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode[];
|
||||
}
|
||||
export type ActionButtonRowProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function ActionButtonRow(props: Props) {
|
||||
const { children } = props;
|
||||
|
||||
return <div className={`${s.row}`}>{children}</div>;
|
||||
}
|
||||
export const ActionButtonRow: FC<ActionButtonRowProps> = ({ children }) => (
|
||||
<div className={`${styles.row}`}>{children}</div>
|
||||
);
|
||||
|
|
|
@ -1,31 +1,29 @@
|
|||
import { Button } from 'antd';
|
||||
import { Button, ButtonProps } from 'antd';
|
||||
import { HeartFilled } from '@ant-design/icons';
|
||||
import { useState } from 'react';
|
||||
import { FC, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import Modal from '../ui/Modal/Modal';
|
||||
import FollowModal from '../modals/FollowModal/FollowModal';
|
||||
import s from './ActionButton/ActionButton.module.scss';
|
||||
import { Modal } from '../ui/Modal/Modal';
|
||||
import { FollowModal } from '../modals/FollowModal/FollowModal';
|
||||
import styles from './ActionButton/ActionButton.module.scss';
|
||||
import { clientConfigStateAtom } from '../stores/ClientConfigStore';
|
||||
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 clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const { name, federation } = clientConfig;
|
||||
const { account } = federation;
|
||||
|
||||
const buttonClicked = () => {
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
{...props}
|
||||
type="primary"
|
||||
className={s.button}
|
||||
className={styles.button}
|
||||
icon={<HeartFilled />}
|
||||
onClick={buttonClicked}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
Follow
|
||||
</Button>
|
||||
|
@ -40,4 +38,4 @@ export default function FollowButton(props: any) {
|
|||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { Button } from 'antd';
|
||||
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 {
|
||||
onClick: () => void;
|
||||
}
|
||||
export type NotifyButtonProps = {
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export default function NotifyButton({ onClick }: Props) {
|
||||
return (
|
||||
<Button type="primary" className={`${s.button}`} icon={<BellFilled />} onClick={onClick}>
|
||||
Notify
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
export const NotifyButton: FC<NotifyButtonProps> = ({ onClick }) => (
|
||||
<Button type="primary" className={`${styles.button}`} icon={<BellFilled />} onClick={onClick}>
|
||||
Notify
|
||||
</Button>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ChatActionMessage from './ChatActionMessage';
|
||||
import { ChatActionMessage } from './ChatActionMessage';
|
||||
import Mock from '../../../stories/assets/mocks/chatmessage-action.png';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -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 */
|
||||
interface Props {
|
||||
export type ChatActionMessageProps = {
|
||||
body: string;
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function ChatActionMessage(props: Props) {
|
||||
const { body } = props;
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: body }} className={s.chatAction} />;
|
||||
}
|
||||
export const ChatActionMessage: FC<ChatActionMessageProps> = ({ body }) => (
|
||||
<div dangerouslySetInnerHTML={{ __html: body }} className={styles.chatAction} />
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import ChatContainer from './index';
|
||||
import { ChatContainer } from './ChatContainer';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,32 +1,77 @@
|
|||
import { Button } from 'antd';
|
||||
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 {
|
||||
ConnectedClientInfoEvent,
|
||||
MessageType,
|
||||
NameChangeEvent,
|
||||
} from '../../../interfaces/socket-events';
|
||||
import s from './ChatContainer.module.scss';
|
||||
import styles from './ChatContainer.module.scss';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import { ChatTextField, ChatUserMessage } from '..';
|
||||
import ChatModeratorNotification from '../ChatModeratorNotification/ChatModeratorNotification';
|
||||
import { ChatUserMessage } from '../ChatUserMessage/ChatUserMessage';
|
||||
import { ChatTextField } from '../ChatTextField/ChatTextField';
|
||||
import { ChatModeratorNotification } from '../ChatModeratorNotification/ChatModeratorNotification';
|
||||
// import ChatActionMessage from '../ChatAction/ChatActionMessage';
|
||||
import ChatSystemMessage from '../ChatSystemMessage/ChatSystemMessage';
|
||||
import ChatJoinMessage from '../ChatJoinMessage/ChatJoinMessage';
|
||||
import { ChatSystemMessage } from '../ChatSystemMessage/ChatSystemMessage';
|
||||
import { ChatJoinMessage } from '../ChatJoinMessage/ChatJoinMessage';
|
||||
|
||||
interface Props {
|
||||
export type ChatContainerProps = {
|
||||
messages: ChatMessage[];
|
||||
usernameToHighlight: string;
|
||||
chatUserId: string;
|
||||
isModerator: boolean;
|
||||
showInput?: boolean;
|
||||
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) {
|
||||
const { messages, usernameToHighlight, chatUserId, isModerator, showInput, height } = props;
|
||||
function checkIsModerator(message) {
|
||||
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 [showButton, setShowButton] = useState(false);
|
||||
const chatContainerRef = useRef(null);
|
||||
|
@ -38,13 +83,13 @@ export default function ChatContainer(props: Props) {
|
|||
const color = `var(--theme-color-users-${displayColor})`;
|
||||
|
||||
return (
|
||||
<div className={s.nameChangeView}>
|
||||
<div className={styles.nameChangeView}>
|
||||
<div style={{ marginRight: 5, height: 'max-content', margin: 'auto 5px auto 0' }}>
|
||||
<EditFilled />
|
||||
</div>
|
||||
<div className={s.nameChangeText}>
|
||||
<div className={styles.nameChangeText}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -129,7 +174,7 @@ export default function ChatContainer(props: Props) {
|
|||
atBottomStateChange={bottom => setAtBottom(bottom)}
|
||||
/>
|
||||
{!atBottom && (
|
||||
<div className={s.toBottomWrap}>
|
||||
<div className={styles.toBottomWrap}>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<VerticalAlignBottomOutlined />}
|
||||
|
@ -161,46 +206,7 @@ export default function ChatContainer(props: Props) {
|
|||
{showInput && <ChatTextField />}
|
||||
</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 = {
|
||||
showInput: true,
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ChatContainer';
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ChatJoinMessage from './ChatJoinMessage';
|
||||
import { ChatJoinMessage } from './ChatJoinMessage';
|
||||
import Mock from '../../../stories/assets/mocks/chatmessage-action.png';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import s from './ChatJoinMessage.module.scss';
|
||||
import ChatUserBadge from '../ChatUserBadge/ChatUserBadge';
|
||||
import { FC } from 'react';
|
||||
import styles from './ChatJoinMessage.module.scss';
|
||||
import { ChatUserBadge } from '../ChatUserBadge/ChatUserBadge';
|
||||
|
||||
interface Props {
|
||||
export type ChatJoinMessageProps = {
|
||||
isAuthorModerator: boolean;
|
||||
userColor: number;
|
||||
displayName: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function ChatJoinMessage(props: Props) {
|
||||
const { isAuthorModerator, userColor, displayName } = props;
|
||||
export const ChatJoinMessage: FC<ChatJoinMessageProps> = ({
|
||||
isAuthorModerator,
|
||||
userColor,
|
||||
displayName,
|
||||
}) => {
|
||||
const color = `var(--theme-user-colors-${userColor})`;
|
||||
|
||||
return (
|
||||
<div className={s.join}>
|
||||
<div className={styles.join}>
|
||||
<span style={{ color }}>
|
||||
{displayName}
|
||||
{isAuthorModerator && (
|
||||
|
@ -24,4 +28,4 @@ export default function ChatJoinMessage(props: Props) {
|
|||
joined the chat.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import ChatModerationActionMenu from './ChatModerationActionMenu';
|
||||
import { ChatModerationActionMenu } from './ChatModerationActionMenu';
|
||||
|
||||
const mocks = {
|
||||
mocks: [
|
||||
|
@ -82,7 +82,7 @@ export default {
|
|||
} as ComponentMeta<typeof ChatModerationActionMenu>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const Template: ComponentStory<typeof ChatModerationActionMenu> = args => (
|
||||
const Template: ComponentStory<typeof ChatModerationActionMenu> = () => (
|
||||
<RecoilRoot>
|
||||
<ChatModerationActionMenu
|
||||
accessToken="abc123"
|
||||
|
|
|
@ -5,22 +5,26 @@ import {
|
|||
SmallDashOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Dropdown, Menu, MenuProps, Space, Modal, message } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import ChatModerationDetailsModal from '../ChatModerationDetailsModal/ChatModerationDetailsModal';
|
||||
import s from './ChatModerationActionMenu.module.scss';
|
||||
import { FC, useState } from 'react';
|
||||
import { ChatModerationDetailsModal } from '../ChatModerationDetailsModal/ChatModerationDetailsModal';
|
||||
import styles from './ChatModerationActionMenu.module.scss';
|
||||
import ChatModeration from '../../../services/moderation-service';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface Props {
|
||||
export type ChatModerationActionMenuProps = {
|
||||
accessToken: string;
|
||||
messageID: string;
|
||||
userID: string;
|
||||
userDisplayName: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function ChatModerationActionMenu(props: Props) {
|
||||
const { messageID, userID, userDisplayName, accessToken } = props;
|
||||
export const ChatModerationActionMenu: FC<ChatModerationActionMenuProps> = ({
|
||||
messageID,
|
||||
userID,
|
||||
userDisplayName,
|
||||
accessToken,
|
||||
}) => {
|
||||
const [showUserDetailsModal, setShowUserDetailsModal] = useState(false);
|
||||
|
||||
const handleBanUser = async () => {
|
||||
|
@ -78,7 +82,7 @@ export default function ChatModerationActionMenu(props: Props) {
|
|||
{
|
||||
label: (
|
||||
<div>
|
||||
<span className={s.icon}>
|
||||
<span className={styles.icon}>
|
||||
<EyeInvisibleOutlined />
|
||||
</span>
|
||||
Hide Message
|
||||
|
@ -89,7 +93,7 @@ export default function ChatModerationActionMenu(props: Props) {
|
|||
{
|
||||
label: (
|
||||
<div>
|
||||
<span className={s.icon}>
|
||||
<span className={styles.icon}>
|
||||
<CloseCircleOutlined />
|
||||
</span>
|
||||
Ban User
|
||||
|
@ -127,4 +131,4 @@ export default function ChatModerationActionMenu(props: Props) {
|
|||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import ChatModerationDetailsModal from './ChatModerationDetailsModal';
|
||||
import { ChatModerationDetailsModal } from './ChatModerationDetailsModal';
|
||||
|
||||
const mocks = {
|
||||
mocks: [
|
||||
|
@ -82,7 +82,7 @@ export default {
|
|||
} as ComponentMeta<typeof ChatModerationDetailsModal>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const Template: ComponentStory<typeof ChatModerationDetailsModal> = args => (
|
||||
const Template: ComponentStory<typeof ChatModerationDetailsModal> = () => (
|
||||
<RecoilRoot>
|
||||
<ChatModerationDetailsModal userId="testuser123" accessToken="fakeaccesstoken4839" />
|
||||
</RecoilRoot>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
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 s from './ChatModerationDetailsModal.module.scss';
|
||||
import styles from './ChatModerationDetailsModal.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type ChatModerationDetailsModalProps = {
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface UserDetails {
|
||||
user: User;
|
||||
|
@ -91,7 +91,7 @@ const UserColorBlock = ({ color }) => {
|
|||
<Row justify="space-around" align="middle">
|
||||
<Col span={12}>Color</Col>
|
||||
<Col span={12}>
|
||||
<div className={s.colorBlock} style={{ backgroundColor: bg }}>
|
||||
<div className={styles.colorBlock} style={{ backgroundColor: bg }}>
|
||||
{color}
|
||||
</div>
|
||||
</Col>
|
||||
|
@ -99,8 +99,10 @@ const UserColorBlock = ({ color }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default function ChatModerationDetailsModal(props: Props) {
|
||||
const { userId, accessToken } = props;
|
||||
export const ChatModerationDetailsModal: FC<ChatModerationDetailsModalProps> = ({
|
||||
userId,
|
||||
accessToken,
|
||||
}) => {
|
||||
const [userDetails, setUserDetails] = useState<UserDetails | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
@ -127,7 +129,7 @@ export default function ChatModerationDetailsModal(props: Props) {
|
|||
user;
|
||||
|
||||
return (
|
||||
<div className={s.modalContainer}>
|
||||
<div className={styles.modalContainer}>
|
||||
<Spin spinning={loading}>
|
||||
<h1>{displayName}</h1>
|
||||
<Row justify="space-around" align="middle">
|
||||
|
@ -161,7 +163,7 @@ export default function ChatModerationDetailsModal(props: Props) {
|
|||
<div>
|
||||
<h1>Recent Chat Messages</h1>
|
||||
|
||||
<div className={s.chatHistory}>
|
||||
<div className={styles.chatHistory}>
|
||||
{messages.map(message => (
|
||||
<ChatMessageRow
|
||||
key={message.id}
|
||||
|
@ -176,4 +178,4 @@ export default function ChatModerationDetailsModal(props: Props) {
|
|||
</Spin>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ChatModeratorNotification from './ChatModeratorNotification';
|
||||
import { ChatModeratorNotification } from './ChatModeratorNotification';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Chat/Messages/Moderation Role Notification',
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import s from './ChatModeratorNotification.module.scss';
|
||||
import styles from './ChatModeratorNotification.module.scss';
|
||||
import Icon from '../../../assets/images/moderator.svg';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function ModeratorNotification() {
|
||||
return (
|
||||
<div className={s.chatModerationNotification}>
|
||||
<Icon className={s.icon} />
|
||||
You are now a moderator.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const ChatModeratorNotification = () => (
|
||||
<div className={styles.chatModerationNotification}>
|
||||
<Icon className={styles.icon} />
|
||||
You are now a moderator.
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ChatSocialMessage from './ChatSocialMessage';
|
||||
import { ChatSocialMessage } from './ChatSocialMessage';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Chat/Messages/Social-fediverse event',
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/* eslint-disable react/no-unused-prop-types */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// TODO remove unused props
|
||||
import { FC } from 'react';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
|
||||
interface Props {
|
||||
export interface ChatSocialMessageProps {
|
||||
message: ChatMessage;
|
||||
}
|
||||
|
||||
export default function ChatSocialMessage(props: Props) {
|
||||
return <div>Component goes here</div>;
|
||||
}
|
||||
export const ChatSocialMessage: FC<ChatSocialMessageProps> = () => <div>Component goes here</div>;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from '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 { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
/* eslint-disable react/no-danger */
|
||||
import { Highlight } from 'react-highlighter-ts';
|
||||
import { FC } from 'react';
|
||||
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;
|
||||
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 (
|
||||
<div className={s.chatSystemMessage}>
|
||||
<div className={s.user}>
|
||||
<span className={s.userName}>{displayName}</span>
|
||||
</div>
|
||||
<Highlight search={highlightString}>
|
||||
<div className={s.message} dangerouslySetInnerHTML={{ __html: body }} />
|
||||
</Highlight>
|
||||
export const ChatSystemMessage: FC<ChatSystemMessageProps> = ({
|
||||
message: {
|
||||
body,
|
||||
user: { displayName },
|
||||
},
|
||||
highlightString,
|
||||
}) => (
|
||||
<div className={styles.chatSystemMessage}>
|
||||
<div className={styles.user}>
|
||||
<span className={styles.userName}>{displayName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<Highlight search={highlightString}>
|
||||
<div className={styles.message} dangerouslySetInnerHTML={{ __html: body }} />
|
||||
</Highlight>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import ChatTextField from './ChatTextField';
|
||||
import { ChatTextField } from './ChatTextField';
|
||||
import Mockup from '../../../stories/assets/mocks/chatinput-mock.png';
|
||||
|
||||
const mockResponse = JSON.parse(
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { SendOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
import { Button, Popover } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
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 EmojiPicker from './EmojiPicker';
|
||||
import { EmojiPicker } from './EmojiPicker';
|
||||
import WebsocketService from '../../../services/websocket-service';
|
||||
import { websocketServiceAtom } from '../../stores/ClientConfigStore';
|
||||
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 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 websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||
const editor = useMemo(() => withReact(withImages(createEditor())), []);
|
||||
|
@ -196,14 +198,13 @@ export default function ChatTextField() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className={style.root}>
|
||||
<div className={styles.root}>
|
||||
<Slate editor={editor} value={defaultEditorValue}>
|
||||
<Editable
|
||||
onKeyDown={onKeyDown}
|
||||
renderElement={renderElement}
|
||||
placeholder="Chat message goes here..."
|
||||
style={{ width: '100%' }}
|
||||
// onChange={change => setValue(change.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<Popover
|
||||
|
@ -221,14 +222,14 @@ export default function ChatTextField() {
|
|||
|
||||
<button
|
||||
type="button"
|
||||
className={style.emojiButton}
|
||||
className={styles.emojiButton}
|
||||
title="Emoji picker button"
|
||||
onClick={() => setShowEmojis(!showEmojis)}
|
||||
>
|
||||
<SmileOutlined />
|
||||
</button>
|
||||
<Button
|
||||
className={style.sendButton}
|
||||
className={styles.sendButton}
|
||||
size="large"
|
||||
type="ghost"
|
||||
icon={<SendOutlined />}
|
||||
|
@ -237,4 +238,4 @@ export default function ChatTextField() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ interface Props {
|
|||
}
|
||||
|
||||
// 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 { onEmojiSelect, onCustomEmojiSelect } = props;
|
||||
const ref = useRef();
|
||||
|
@ -54,4 +54,4 @@ export default function EmojiPicker(props: Props) {
|
|||
}, [customEmoji]);
|
||||
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ChatUserBadge from './ChatUserBadge';
|
||||
import { ChatUserBadge } from './ChatUserBadge';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Chat/Messages/User Flag',
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import React from 'react';
|
||||
import s from './ChatUserBadge.module.scss';
|
||||
import React, { FC } from 'react';
|
||||
import styles from './ChatUserBadge.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type ChatUserBadgeProps = {
|
||||
badge: React.ReactNode;
|
||||
userColor: number;
|
||||
}
|
||||
};
|
||||
|
||||
export default function ChatUserBadge(props: Props) {
|
||||
const { badge, userColor } = props;
|
||||
export const ChatUserBadge: FC<ChatUserBadgeProps> = ({ badge, userColor }) => {
|
||||
const color = `var(--theme-user-colors-${userColor})`;
|
||||
const style = { color, borderColor: color };
|
||||
|
||||
return (
|
||||
<span style={style} className={s.badge}>
|
||||
<span style={style} className={styles.badge}>
|
||||
{badge}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import ChatUserMessage from './index';
|
||||
import { ChatUserMessage } from './ChatUserMessage';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import Mock from '../../../stories/assets/mocks/chatmessage-user.png';
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
/* eslint-disable react/no-danger */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { Highlight } from 'react-highlighter-ts';
|
||||
import he from 'he';
|
||||
import cn from 'classnames';
|
||||
import { Tooltip } from 'antd';
|
||||
import { LinkOutlined } from '@ant-design/icons';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import s from './ChatUserMessage.module.scss';
|
||||
import styles from './ChatUserMessage.module.scss';
|
||||
import { formatTimestamp } from './messageFmt';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import ChatModerationActionMenu from '../ChatModerationActionMenu/ChatModerationActionMenu';
|
||||
import ChatUserBadge from '../ChatUserBadge/ChatUserBadge';
|
||||
import { ChatModerationActionMenu } from '../ChatModerationActionMenu/ChatModerationActionMenu';
|
||||
import { ChatUserBadge } from '../ChatUserBadge/ChatUserBadge';
|
||||
import { accessTokenAtom } from '../../stores/ClientConfigStore';
|
||||
|
||||
interface Props {
|
||||
export type ChatUserMessageProps = {
|
||||
message: ChatMessage;
|
||||
showModeratorMenu: boolean;
|
||||
highlightString: string;
|
||||
|
@ -21,9 +21,9 @@ interface Props {
|
|||
sameUserAsLast: boolean;
|
||||
isAuthorModerator: boolean;
|
||||
isAuthorAuthenticated: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export default function ChatUserMessage({
|
||||
export const ChatUserMessage: FC<ChatUserMessageProps> = ({
|
||||
message,
|
||||
highlightString,
|
||||
showModeratorMenu,
|
||||
|
@ -31,7 +31,7 @@ export default function ChatUserMessage({
|
|||
sameUserAsLast,
|
||||
isAuthorModerator,
|
||||
isAuthorAuthenticated,
|
||||
}: Props) {
|
||||
}) => {
|
||||
const { id: messageId, body, user, timestamp } = message;
|
||||
const { id: userId, displayName, displayColor } = user;
|
||||
const accessToken = useRecoilValue<string>(accessTokenAtom);
|
||||
|
@ -59,29 +59,32 @@ export default function ChatUserMessage({
|
|||
}, [message]);
|
||||
|
||||
return (
|
||||
<div className={cn(s.messagePadding, sameUserAsLast && s.messagePaddingCollapsed)}>
|
||||
<div className={cn(styles.messagePadding, sameUserAsLast && styles.messagePaddingCollapsed)}>
|
||||
<div
|
||||
className={cn(s.root, {
|
||||
[s.ownMessage]: sentBySelf,
|
||||
className={cn(styles.root, {
|
||||
[styles.ownMessage]: sentBySelf,
|
||||
})}
|
||||
style={{ borderColor: color }}
|
||||
>
|
||||
{!sameUserAsLast && (
|
||||
<Tooltip title="user info goes here" placement="topLeft" mouseEnterDelay={1}>
|
||||
<div className={s.user} style={{ color }}>
|
||||
<span className={s.userName}>{displayName}</span>
|
||||
<div className={styles.user} style={{ color }}>
|
||||
<span className={styles.userName}>{displayName}</span>
|
||||
<span>{badgeNodes}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={formattedTimestamp} mouseEnterDelay={1}>
|
||||
<Highlight search={highlightString}>
|
||||
<div className={s.message} dangerouslySetInnerHTML={{ __html: formattedMessage }} />
|
||||
<div
|
||||
className={styles.message}
|
||||
dangerouslySetInnerHTML={{ __html: formattedMessage }}
|
||||
/>
|
||||
</Highlight>
|
||||
</Tooltip>
|
||||
|
||||
{showModeratorMenu && (
|
||||
<div className={s.modMenuWrapper}>
|
||||
<div className={styles.modMenuWrapper}>
|
||||
<ChatModerationActionMenu
|
||||
messageID={messageId}
|
||||
accessToken={accessToken}
|
||||
|
@ -90,9 +93,9 @@ export default function ChatUserMessage({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={s.customBorder} style={{ color }} />
|
||||
<div className={s.background} style={{ color }} />
|
||||
<div className={styles.customBorder} style={{ color }} />
|
||||
<div className={styles.background} style={{ color }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ChatUserMessage';
|
|
@ -1,3 +0,0 @@
|
|||
export { default as ChatContainer } from './ChatContainer';
|
||||
export { default as ChatUserMessage } from './ChatUserMessage';
|
||||
export { default as ChatTextField } from './ChatTextField/ChatTextField';
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ContentHeader from './ContentHeader';
|
||||
import { ContentHeader } from './ContentHeader';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Components/Content Header',
|
||||
|
|
|
@ -1,36 +1,42 @@
|
|||
import cn from 'classnames';
|
||||
|
||||
import { ServerLogo } from '../../ui';
|
||||
import SocialLinks from '../../ui/SocialLinks/SocialLinks';
|
||||
import { FC } from 'react';
|
||||
import { Logo } from '../../ui/Logo/Logo';
|
||||
import { SocialLinks } from '../../ui/SocialLinks/SocialLinks';
|
||||
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;
|
||||
title: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
links: SocialLink[];
|
||||
logo: string;
|
||||
}
|
||||
export default function ContentHeader({ name, title, summary, logo, tags, links }: Props) {
|
||||
return (
|
||||
<div className={s.root}>
|
||||
<div className={s.logoTitleSection}>
|
||||
<div className={s.logo}>
|
||||
<ServerLogo src={logo} />
|
||||
};
|
||||
|
||||
export const ContentHeader: FC<ContentHeaderProps> = ({
|
||||
name,
|
||||
title,
|
||||
summary,
|
||||
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} </span>)}
|
||||
</div>
|
||||
<div className={s.titleSection}>
|
||||
<div className={cn(s.title, s.row)}>{name}</div>
|
||||
<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} </span>)}
|
||||
</div>
|
||||
<div className={cn(s.socialLinks, s.row)}>
|
||||
<SocialLinks links={links} />
|
||||
</div>
|
||||
<div className={cn(styles.socialLinks, styles.row)}>
|
||||
<SocialLinks links={links} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ContentHeader';
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Logo';
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import OwncastLogo from './Logo';
|
||||
import { OwncastLogo } from './OwncastLogo';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Components/Header Logo',
|
|
@ -1,15 +1,15 @@
|
|||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
import s from './Logo.module.scss';
|
||||
import styles from './OwncastLogo.module.scss';
|
||||
|
||||
interface Props {
|
||||
export type LogoProps = {
|
||||
variant: 'simple' | 'contrast';
|
||||
}
|
||||
};
|
||||
|
||||
export default function Logo({ variant = 'simple' }: Props) {
|
||||
const rootClassName = cn(s.root, {
|
||||
[s.simple]: variant === 'simple',
|
||||
[s.contrast]: variant === 'contrast',
|
||||
export const OwncastLogo: FC<LogoProps> = ({ variant = 'simple' }) => {
|
||||
const rootClassName = cn(styles.root, {
|
||||
[styles.simple]: variant === 'simple',
|
||||
[styles.contrast]: variant === 'contrast',
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -169,4 +169,4 @@ export default function Logo({ variant = 'simple' }: Props) {
|
|||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import UserDropdown from './UserDropdown';
|
||||
import { UserDropdown } from './UserDropdown';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Components/User settings menu',
|
||||
|
|
|
@ -7,24 +7,24 @@ import {
|
|||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useState } from 'react';
|
||||
import { FC, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import Modal from '../../ui/Modal/Modal';
|
||||
import { Modal } from '../../ui/Modal/Modal';
|
||||
import {
|
||||
chatVisibleToggleAtom,
|
||||
chatDisplayNameAtom,
|
||||
appStateAtom,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
import s from './UserDropdown.module.scss';
|
||||
import NameChangeModal from '../../modals/NameChangeModal/NameChangeModal';
|
||||
import styles from './UserDropdown.module.scss';
|
||||
import { NameChangeModal } from '../../modals/NameChangeModal/NameChangeModal';
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
export default function UserDropdown({ username: defaultUsername }: Props) {
|
||||
export const UserDropdown: FC<UserDropdownProps> = ({ username: defaultUsername = undefined }) => {
|
||||
const username = defaultUsername || useRecoilValue(chatDisplayNameAtom);
|
||||
const [showNameChangeModal, setShowNameChangeModal] = useState<boolean>(false);
|
||||
const [showAuthModal, setShowAuthModal] = useState<boolean>(false);
|
||||
|
@ -66,7 +66,7 @@ export default function UserDropdown({ username: defaultUsername }: Props) {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={`${s.root}`}>
|
||||
<div className={`${styles.root}`}>
|
||||
<Dropdown overlay={menu} trigger={['click']}>
|
||||
<Button type="primary" icon={<UserOutlined style={{ marginRight: '.5rem' }} />}>
|
||||
<Space>
|
||||
|
@ -91,8 +91,4 @@ export default function UserDropdown({ username: defaultUsername }: Props) {
|
|||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UserDropdown.defaultProps = {
|
||||
username: undefined,
|
||||
};
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export { default } from './UserDropdown';
|
|
@ -1,2 +0,0 @@
|
|||
export { default as UserDropdown } from './UserDropdown';
|
||||
export { default as OwncastLogo } from './Logo';
|
|
@ -1,5 +1,5 @@
|
|||
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 {
|
||||
API_VIDEO_CODEC,
|
||||
|
@ -13,10 +13,11 @@ import {
|
|||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
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 default function CodecSelector() {
|
||||
export type CodecSelectorProps = {};
|
||||
|
||||
export const CodecSelector: FC<CodecSelectorProps> = () => {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
const { videoCodec, supportedCodecs } = serverConfig || {};
|
||||
|
@ -170,4 +171,4 @@ export default function CodecSelector() {
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
// 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 { ColumnsType } from 'antd/lib/table';
|
||||
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 { UpdateArgs, VideoVariant } from '../../types/config-section';
|
||||
|
||||
import VideoVariantForm from './video-variant-form';
|
||||
import { VideoVariantForm } from './VideoVariantForm';
|
||||
import {
|
||||
API_VIDEO_VARIANTS,
|
||||
DEFAULT_VARIANT_STATE,
|
||||
|
@ -24,11 +24,11 @@ import {
|
|||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
import FormStatusIndicator from './form-status-indicator';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function CurrentVariantsTable() {
|
||||
export const CurrentVariantsTable: FC = () => {
|
||||
const [displayModal, setDisplayModal] = useState(false);
|
||||
const [modalProcessing, setModalProcessing] = useState(false);
|
||||
const [editId, setEditId] = useState(0);
|
||||
|
@ -242,4 +242,4 @@ export default function CurrentVariantsTable() {
|
|||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
// 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 { ServerStatusContext } from '../../utils/server-status-context';
|
||||
|
@ -15,14 +15,13 @@ import {
|
|||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
import FormStatusIndicator from './form-status-indicator';
|
||||
|
||||
import TextField, { TEXTFIELD_TYPE_TEXTAREA } from './form-textfield';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
import { TextField, TEXTFIELD_TYPE_TEXTAREA } from './TextField';
|
||||
import { UpdateArgs } from '../../types/config-section';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function EditCustomStyles() {
|
||||
export const EditCustomStyles: FC = () => {
|
||||
const [content, setContent] = useState('');
|
||||
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||
const [hasChanged, setHasChanged] = useState(false);
|
||||
|
@ -114,4 +113,4 @@ export default function EditCustomStyles() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import React, { useState, useContext, useEffect, FC } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
import TextFieldWithSubmit, {
|
||||
import {
|
||||
TextFieldWithSubmit,
|
||||
TEXTFIELD_TYPE_TEXTAREA,
|
||||
TEXTFIELD_TYPE_URL,
|
||||
} from './form-textfield-with-submit';
|
||||
|
||||
} from './TextFieldWithSubmit';
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import {
|
||||
postConfigUpdateToAPI,
|
||||
|
@ -18,14 +17,13 @@ import {
|
|||
FIELD_PROPS_NSFW,
|
||||
FIELD_PROPS_HIDE_VIEWER_COUNT,
|
||||
} from '../../utils/config-constants';
|
||||
|
||||
import { UpdateArgs } from '../../types/config-section';
|
||||
import ToggleSwitch from './form-toggleswitch';
|
||||
import EditLogo from './edit-logo';
|
||||
import { ToggleSwitch } from './ToggleSwitch';
|
||||
import { EditLogo } from './EditLogo';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function EditInstanceDetails() {
|
||||
export const EditInstanceDetails: FC = () => {
|
||||
const [formDataValues, setFormDataValues] = useState(null);
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig } = serverStatusData || {};
|
||||
|
@ -163,4 +161,4 @@ export default function EditInstanceDetails() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,17 +1,10 @@
|
|||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { Button, Tooltip, Collapse, Typography } from 'antd';
|
||||
import { CopyOutlined, RedoOutlined } from '@ant-design/icons';
|
||||
|
||||
import {
|
||||
TEXTFIELD_TYPE_NUMBER,
|
||||
TEXTFIELD_TYPE_PASSWORD,
|
||||
TEXTFIELD_TYPE_URL,
|
||||
} from './form-textfield';
|
||||
import TextFieldWithSubmit from './form-textfield-with-submit';
|
||||
|
||||
import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD, TEXTFIELD_TYPE_URL } from './TextField';
|
||||
import { TextFieldWithSubmit } from './TextFieldWithSubmit';
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import { AlertMessageContext } from '../../utils/alert-message-context';
|
||||
|
||||
import {
|
||||
TEXTFIELD_PROPS_FFMPEG,
|
||||
TEXTFIELD_PROPS_RTMP_PORT,
|
||||
|
@ -19,13 +12,12 @@ import {
|
|||
TEXTFIELD_PROPS_STREAM_KEY,
|
||||
TEXTFIELD_PROPS_WEB_PORT,
|
||||
} from '../../utils/config-constants';
|
||||
|
||||
import { UpdateArgs } from '../../types/config-section';
|
||||
import ResetYP from './reset-yp';
|
||||
import { ResetYP } from './ResetYP';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
export default function EditInstanceDetails() {
|
||||
export const EditInstanceDetails = () => {
|
||||
const [formDataValues, setFormDataValues] = useState(null);
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { setMessage } = useContext(AlertMessageContext);
|
||||
|
@ -164,4 +156,4 @@ export default function EditInstanceDetails() {
|
|||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,14 +1,13 @@
|
|||
/* 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 { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import {
|
||||
FIELD_PROPS_TAGS,
|
||||
RESET_TIMEOUT,
|
||||
postConfigUpdateToAPI,
|
||||
} from '../../utils/config-constants';
|
||||
import TextField from './form-textfield';
|
||||
import { TextField } from './TextField';
|
||||
import { UpdateArgs } from '../../types/config-section';
|
||||
import {
|
||||
createInputStatus,
|
||||
|
@ -18,11 +17,11 @@ import {
|
|||
STATUS_SUCCESS,
|
||||
STATUS_WARNING,
|
||||
} from '../../utils/input-statuses';
|
||||
import { TAG_COLOR } from './edit-string-array';
|
||||
import { TAG_COLOR } from './EditValueArray';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function EditInstanceTags() {
|
||||
export const EditInstanceTags: FC = () => {
|
||||
const [newTagInput, setNewTagInput] = useState<string>('');
|
||||
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||
|
||||
|
@ -136,4 +135,4 @@ export default function EditInstanceTags() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { Button, Upload } from 'antd';
|
||||
import { RcFile } from 'antd/lib/upload/interface';
|
||||
import { LoadingOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import React, { useState, useContext } from 'react';
|
||||
import FormStatusIndicator from './form-status-indicator';
|
||||
import React, { useState, useContext, FC } from 'react';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import {
|
||||
postConfigUpdateToAPI,
|
||||
|
@ -26,7 +26,7 @@ function getBase64(img: File | Blob, callback: (imageUrl: string | ArrayBuffer)
|
|||
reader.readAsDataURL(img);
|
||||
}
|
||||
|
||||
export default function EditLogo() {
|
||||
export const EditLogo: FC = () => {
|
||||
const [logoUrl, setlogoUrl] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logoCachedbuster, setLogoCacheBuster] = useState(0);
|
||||
|
@ -125,4 +125,4 @@ export default function EditLogo() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
// 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 dynamic from 'next/dynamic';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
|
@ -17,7 +17,7 @@ import {
|
|||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
import FormStatusIndicator from './form-status-indicator';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
|
||||
import 'react-markdown-editor-lite/lib/index.css';
|
||||
|
||||
|
@ -28,7 +28,7 @@ const MdEditor = dynamic(() => import('react-markdown-editor-lite'), {
|
|||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function EditPageContent() {
|
||||
export const EditPageContent: FC = () => {
|
||||
const [content, setContent] = useState('');
|
||||
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||
const [hasChanged, setHasChanged] = useState(false);
|
||||
|
@ -122,4 +122,4 @@ export default function EditPageContent() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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 { ColumnsType } from 'antd/lib/table';
|
||||
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 { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import {
|
||||
|
@ -14,13 +14,13 @@ import {
|
|||
} from '../../utils/config-constants';
|
||||
import { SocialHandle, UpdateArgs } from '../../types/config-section';
|
||||
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 FormStatusIndicator from './form-status-indicator';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function EditSocialLinks() {
|
||||
export const EditSocialLinks: FC = () => {
|
||||
const [availableIconsList, setAvailableIconsList] = useState([]);
|
||||
const [currentSocialHandles, setCurrentSocialHandles] = useState([]);
|
||||
|
||||
|
@ -316,4 +316,4 @@ export default function EditSocialLinks() {
|
|||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { Button, Collapse } from 'antd';
|
||||
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 { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import { AlertMessageContext } from '../../utils/alert-message-context';
|
||||
|
@ -18,10 +18,10 @@ import {
|
|||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
import TextField from './form-textfield';
|
||||
import FormStatusIndicator from './form-status-indicator';
|
||||
import { TextField } from './TextField';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
import isValidUrl from '../../utils/urls';
|
||||
import ToggleSwitch from './form-toggleswitch';
|
||||
import { ToggleSwitch } from './ToggleSwitch';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
|
@ -63,7 +63,7 @@ function checkSaveable(formValues: any, currentValues: any) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export default function EditStorage() {
|
||||
export const EditStorage: FC = () => {
|
||||
const [formDataValues, setFormDataValues] = useState(null);
|
||||
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||
|
||||
|
@ -254,4 +254,4 @@ export default function EditStorage() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,17 +1,17 @@
|
|||
/* eslint-disable react/no-array-index-key */
|
||||
import React, { useState } from 'react';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Typography, Tag } from 'antd';
|
||||
|
||||
import TextField from './form-textfield';
|
||||
import { TextField } from './TextField';
|
||||
import { UpdateArgs } from '../../types/config-section';
|
||||
import { StatusState } from '../../utils/input-statuses';
|
||||
import FormStatusIndicator from './form-status-indicator';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export const TAG_COLOR = '#5a67d8';
|
||||
|
||||
interface EditStringArrayProps {
|
||||
export type EditStringArrayProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
placeholder: string;
|
||||
|
@ -21,21 +21,20 @@ interface EditStringArrayProps {
|
|||
continuousStatusMessage?: StatusState;
|
||||
handleDeleteIndex: (index: number) => 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 {
|
||||
title,
|
||||
description,
|
||||
placeholder,
|
||||
maxLength,
|
||||
values,
|
||||
handleDeleteIndex,
|
||||
handleCreateString,
|
||||
submitStatus,
|
||||
continuousStatusMessage,
|
||||
} = props;
|
||||
|
||||
const handleInputChange = ({ value }: UpdateArgs) => {
|
||||
setNewStringInput(value);
|
||||
|
@ -84,7 +83,7 @@ export default function EditValueArray(props: EditStringArrayProps) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
EditValueArray.defaultProps = {
|
||||
maxLength: 50,
|
|
@ -1,15 +1,14 @@
|
|||
// 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 ToggleSwitch from './form-toggleswitch';
|
||||
|
||||
import { ToggleSwitch } from './ToggleSwitch';
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import { FIELD_PROPS_NSFW, FIELD_PROPS_YP } from '../../utils/config-constants';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function EditYPDetails() {
|
||||
export const EditYPDetails: FC = () => {
|
||||
const [formDataValues, setFormDataValues] = useState(null);
|
||||
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
|
@ -68,4 +67,4 @@ export default function EditYPDetails() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,12 +1,13 @@
|
|||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { StatusState } from '../../utils/input-statuses';
|
||||
|
||||
interface FormStatusIndicatorProps {
|
||||
export type FormStatusIndicatorProps = {
|
||||
status: StatusState;
|
||||
}
|
||||
export default function FormStatusIndicator({ status }: FormStatusIndicatorProps) {
|
||||
};
|
||||
|
||||
export const FormStatusIndicator: FC<FormStatusIndicatorProps> = ({ status }) => {
|
||||
const { type, icon, message } = status || {};
|
||||
const classes = classNames({
|
||||
'status-container': true,
|
||||
|
@ -19,4 +20,5 @@ export default function FormStatusIndicator({ status }: FormStatusIndicatorProps
|
|||
{message ? <span className="status-message">{message}</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default FormStatusIndicator;
|
|
@ -1,5 +1,5 @@
|
|||
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 { API_YP_RESET, fetchData } from '../../utils/apis';
|
||||
|
@ -10,9 +10,9 @@ import {
|
|||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} 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 [submitStatus, setSubmitStatus] = useState(null);
|
||||
|
@ -61,4 +61,4 @@ export default function ResetYP() {
|
|||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,16 +1,16 @@
|
|||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { SocialHandleDropdownItem } from '../../types/config-section';
|
||||
import { NEXT_PUBLIC_API_HOST } from '../../utils/apis';
|
||||
import { OTHER_SOCIAL_HANDLE_OPTION } from '../../utils/config-constants';
|
||||
|
||||
interface DropdownProps {
|
||||
export type DropdownProps = {
|
||||
iconList: SocialHandleDropdownItem[];
|
||||
selectedOption: string;
|
||||
onSelected: any;
|
||||
}
|
||||
};
|
||||
|
||||
export default function SocialDropdown({ iconList, selectedOption, onSelected }: DropdownProps) {
|
||||
export const SocialDropdown: FC<DropdownProps> = ({ iconList, selectedOption, onSelected }) => {
|
||||
const handleSelected = (value: string) => {
|
||||
if (onSelected) {
|
||||
onSelected(value);
|
||||
|
@ -62,4 +62,4 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Input, InputNumber } from 'antd';
|
||||
import { FieldUpdaterFunc } from '../../types/config-section';
|
||||
// import InfoTip from '../info-tip';
|
||||
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_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_URL = 'url';
|
||||
|
||||
export interface TextFieldProps {
|
||||
export type TextFieldProps = {
|
||||
fieldName: string;
|
||||
|
||||
onSubmit?: () => void;
|
||||
|
@ -33,28 +33,26 @@ export interface TextFieldProps {
|
|||
value?: string | number;
|
||||
onBlur?: 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) => {
|
||||
// if an extra onChange handler was sent in as a prop, let's run that too.
|
||||
if (onChange) {
|
||||
|
@ -151,7 +149,8 @@ export default function TextField(props: TextFieldProps) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default TextField;
|
||||
|
||||
TextField.defaultProps = {
|
||||
className: '',
|
|
@ -1,6 +1,6 @@
|
|||
import { Button } from 'antd';
|
||||
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 { postConfigUpdateToAPI, RESET_TIMEOUT } from '../../utils/config-constants';
|
||||
import {
|
||||
|
@ -11,8 +11,8 @@ import {
|
|||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import FormStatusIndicator from './form-status-indicator';
|
||||
import TextField, { TextFieldProps } from './form-textfield';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
import { TextField, TextFieldProps } from './TextField';
|
||||
|
||||
export const TEXTFIELD_TYPE_TEXT = 'default';
|
||||
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_URL = 'url';
|
||||
|
||||
interface TextFieldWithSubmitProps extends TextFieldProps {
|
||||
export type TextFieldWithSubmitProps = TextFieldProps & {
|
||||
apiPath: string;
|
||||
configPath?: 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 [hasChanged, setHasChanged] = useState(false);
|
||||
|
@ -36,15 +43,6 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
|
|||
|
||||
let resetTimer = null;
|
||||
|
||||
const {
|
||||
apiPath,
|
||||
configPath = '',
|
||||
initialValue,
|
||||
useTrim,
|
||||
useTrimLead,
|
||||
...textFieldProps // rest of props
|
||||
} = props;
|
||||
|
||||
const { fieldName, required, tip, status, value, onChange, onSubmit } = textFieldProps;
|
||||
|
||||
// Clear out any validation states and messaging
|
||||
|
@ -150,7 +148,7 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
TextFieldWithSubmit.defaultProps = {
|
||||
configPath: '',
|
|
@ -2,7 +2,7 @@
|
|||
// This one is styled to match the form-textfield component.
|
||||
// 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 {
|
||||
createInputStatus,
|
||||
|
@ -11,13 +11,12 @@ import {
|
|||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
import FormStatusIndicator from './form-status-indicator';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
|
||||
import { RESET_TIMEOUT, postConfigUpdateToAPI } from '../../utils/config-constants';
|
||||
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
|
||||
interface ToggleSwitchProps {
|
||||
export type ToggleSwitchProps = {
|
||||
fieldName: string;
|
||||
|
||||
apiPath?: string;
|
||||
|
@ -29,8 +28,20 @@ interface ToggleSwitchProps {
|
|||
tip?: string;
|
||||
useSubmit?: boolean;
|
||||
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);
|
||||
|
||||
let resetTimer = null;
|
||||
|
@ -38,19 +49,6 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
|
|||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { setFieldInConfigState } = serverStatusData || {};
|
||||
|
||||
const {
|
||||
apiPath,
|
||||
checked,
|
||||
reversed = false,
|
||||
configPath = '',
|
||||
disabled = false,
|
||||
fieldName,
|
||||
label,
|
||||
tip,
|
||||
useSubmit,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
const resetStates = () => {
|
||||
setSubmitStatus(null);
|
||||
clearTimeout(resetTimer);
|
||||
|
@ -107,7 +105,8 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default ToggleSwitch;
|
||||
|
||||
ToggleSwitch.defaultProps = {
|
||||
apiPath: '',
|
|
@ -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 { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import { AlertMessageContext } from '../../utils/alert-message-context';
|
||||
|
@ -14,7 +14,7 @@ import {
|
|||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
import FormStatusIndicator from './form-status-indicator';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
|
@ -34,7 +34,7 @@ const SLIDER_COMMENTS = {
|
|||
4: 'Highest latency, highest error tolerance',
|
||||
};
|
||||
|
||||
export default function VideoLatency() {
|
||||
export const VideoLatency: FC = () => {
|
||||
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||
const [selectedOption, setSelectedOption] = useState(null);
|
||||
|
||||
|
@ -130,4 +130,4 @@ export default function VideoLatency() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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.
|
||||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { Popconfirm, Row, Col, Slider, Collapse, Typography } from 'antd';
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
import { FieldUpdaterFunc, VideoVariant, UpdateArgs } from '../../types/config-section';
|
||||
import TextField from './form-textfield';
|
||||
import { TextField } from './TextField';
|
||||
import {
|
||||
DEFAULT_VARIANT_STATE,
|
||||
VIDEO_VARIANT_SETTING_DEFAULTS,
|
||||
|
@ -17,19 +17,19 @@ import {
|
|||
FRAMERATE_DEFAULTS,
|
||||
FRAMERATE_TOOLTIPS,
|
||||
} from '../../utils/config-constants';
|
||||
import ToggleSwitch from './form-toggleswitch';
|
||||
import { ToggleSwitch } from './ToggleSwitch';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
interface VideoVariantFormProps {
|
||||
export type VideoVariantFormProps = {
|
||||
dataState: VideoVariant;
|
||||
onUpdateField: FieldUpdaterFunc;
|
||||
}
|
||||
};
|
||||
|
||||
export default function VideoVariantForm({
|
||||
export const VideoVariantForm: FC<VideoVariantFormProps> = ({
|
||||
dataState = DEFAULT_VARIANT_STATE,
|
||||
onUpdateField,
|
||||
}: VideoVariantFormProps) {
|
||||
}) => {
|
||||
const videoPassthroughEnabled = dataState.videoPassthrough;
|
||||
|
||||
const handleFramerateChange = (value: number) => {
|
||||
|
@ -314,4 +314,4 @@ export default function VideoVariantForm({
|
|||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,13 +1,13 @@
|
|||
import { Button, Typography } from 'antd';
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||
import TextField, { TEXTFIELD_TYPE_TEXTAREA } from '../form-textfield';
|
||||
import { TextField, TEXTFIELD_TYPE_TEXTAREA } from '../TextField';
|
||||
import {
|
||||
postConfigUpdateToAPI,
|
||||
RESET_TIMEOUT,
|
||||
BROWSER_PUSH_CONFIG_FIELDS,
|
||||
} from '../../../utils/config-constants';
|
||||
import ToggleSwitch from '../form-toggleswitch';
|
||||
import { ToggleSwitch } from '../ToggleSwitch';
|
||||
import {
|
||||
createInputStatus,
|
||||
StatusState,
|
||||
|
@ -15,11 +15,11 @@ import {
|
|||
STATUS_SUCCESS,
|
||||
} from '../../../utils/input-statuses';
|
||||
import { UpdateArgs } from '../../../types/config-section';
|
||||
import FormStatusIndicator from '../form-status-indicator';
|
||||
import { FormStatusIndicator } from '../FormStatusIndicator';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function ConfigNotify() {
|
||||
export const ConfigNotify = () => {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
const { notifications } = serverConfig || {};
|
||||
|
@ -126,4 +126,5 @@ export default function ConfigNotify() {
|
|||
<FormStatusIndicator status={submitStatus} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default ConfigNotify;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { Button, Typography } from 'antd';
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||
import TextField from '../form-textfield';
|
||||
import FormStatusIndicator from '../form-status-indicator';
|
||||
import { TextField } from '../TextField';
|
||||
import { FormStatusIndicator } from '../FormStatusIndicator';
|
||||
import {
|
||||
postConfigUpdateToAPI,
|
||||
RESET_TIMEOUT,
|
||||
DISCORD_CONFIG_FIELDS,
|
||||
} from '../../../utils/config-constants';
|
||||
import ToggleSwitch from '../form-toggleswitch';
|
||||
import { ToggleSwitch } from '../ToggleSwitch';
|
||||
import {
|
||||
createInputStatus,
|
||||
StatusState,
|
||||
|
@ -19,7 +19,7 @@ import { UpdateArgs } from '../../../types/config-section';
|
|||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function ConfigNotify() {
|
||||
export const ConfigNotify = () => {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
const { notifications } = serverConfig || {};
|
||||
|
@ -150,4 +150,5 @@ export default function ConfigNotify() {
|
|||
<FormStatusIndicator status={submitStatus} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default ConfigNotify;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ServerStatusContext } from '../../../utils/server-status-context';
|
|||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function ConfigNotify() {
|
||||
export const ConfigNotify = () => {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig } = serverStatusData || {};
|
||||
const { federation } = serverConfig || {};
|
||||
|
@ -48,4 +48,5 @@ export default function ConfigNotify() {
|
|||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default ConfigNotify;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { Button, Typography } from 'antd';
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||
import TextField, { TEXTFIELD_TYPE_PASSWORD } from '../form-textfield';
|
||||
import FormStatusIndicator from '../form-status-indicator';
|
||||
import { TextField, TEXTFIELD_TYPE_PASSWORD } from '../TextField';
|
||||
import { FormStatusIndicator } from '../FormStatusIndicator';
|
||||
import {
|
||||
postConfigUpdateToAPI,
|
||||
RESET_TIMEOUT,
|
||||
TWITTER_CONFIG_FIELDS,
|
||||
} from '../../../utils/config-constants';
|
||||
import ToggleSwitch from '../form-toggleswitch';
|
||||
import { ToggleSwitch } from '../ToggleSwitch';
|
||||
import {
|
||||
createInputStatus,
|
||||
StatusState,
|
||||
|
@ -16,11 +16,11 @@ import {
|
|||
STATUS_SUCCESS,
|
||||
} from '../../../utils/input-statuses';
|
||||
import { UpdateArgs } from '../../../types/config-section';
|
||||
import { TEXTFIELD_TYPE_TEXT } from '../form-textfield-with-submit';
|
||||
import { TEXTFIELD_TYPE_TEXT } from '../TextFieldWithSubmit';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function ConfigNotify() {
|
||||
export const ConfigNotify = () => {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
const { notifications } = serverConfig || {};
|
||||
|
@ -222,4 +222,5 @@ export default function ConfigNotify() {
|
|||
<FormStatusIndicator status={submitStatus} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default ConfigNotify;
|
||||
|
|
15
web/components/layouts/AdminLayout.tsx
Normal file
15
web/components/layouts/AdminLayout.tsx
Normal 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>
|
||||
);
|
|
@ -1,20 +1,21 @@
|
|||
import { Layout } from 'antd';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import Head from 'next/head';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import {
|
||||
ClientConfigStore,
|
||||
isChatAvailableSelector,
|
||||
clientConfigStateAtom,
|
||||
fatalErrorStateAtom,
|
||||
} 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 { DisplayableError } from '../../types/displayable-error';
|
||||
import FatalErrorStateModal from '../modals/FatalErrorStateModal/FatalErrorStateModal';
|
||||
import { FatalErrorStateModal } from '../modals/FatalErrorStateModal/FatalErrorStateModal';
|
||||
import setupNoLinkReferrer from '../../utils/no-link-referrer';
|
||||
|
||||
function Main() {
|
||||
export const Main: FC = () => {
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const { name, title, customStyles } = clientConfig;
|
||||
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
|
||||
|
@ -97,6 +98,4 @@ function Main() {
|
|||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Main;
|
||||
};
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { AppProps } from 'next/app';
|
||||
import { FC } from 'react';
|
||||
|
||||
function SimpleLayout({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<div>
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SimpleLayout;
|
||||
export const SimpleLayout: FC<AppProps> = ({ Component, pageProps }) => (
|
||||
<div>
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import AuthModal from './AuthModal';
|
||||
import { AuthModal } from './AuthModal';
|
||||
|
||||
const Example = () => (
|
||||
<div>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Tabs } from 'antd';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import IndieAuthModal from '../IndieAuthModal/IndieAuthModal';
|
||||
import FediAuthModal from '../FediAuthModal/FediAuthModal';
|
||||
import { FC } from 'react';
|
||||
import { IndieAuthModal } from '../IndieAuthModal/IndieAuthModal';
|
||||
import { FediAuthModal } from '../FediAuthModal/FediAuthModal';
|
||||
|
||||
import FediverseIcon from '../../../assets/images/fediverse-black.png';
|
||||
import IndieAuthIcon from '../../../assets/images/indieauth.png';
|
||||
|
||||
import s from './AuthModal.module.scss';
|
||||
import styles from './AuthModal.module.scss';
|
||||
import {
|
||||
chatDisplayNameAtom,
|
||||
chatAuthenticatedAtom,
|
||||
|
@ -15,10 +16,7 @@ import {
|
|||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
interface Props {}
|
||||
|
||||
export default function AuthModal(props: Props) {
|
||||
export const AuthModal: FC = () => {
|
||||
const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom);
|
||||
const authenticated = useRecoilValue<boolean>(chatAuthenticatedAtom);
|
||||
const accessToken = useRecoilValue<string>(accessTokenAtom);
|
||||
|
@ -34,8 +32,8 @@ export default function AuthModal(props: Props) {
|
|||
>
|
||||
<TabPane
|
||||
tab={
|
||||
<span className={s.tabContent}>
|
||||
<img className={s.icon} src={IndieAuthIcon.src} alt="IndieAuth" />
|
||||
<span className={styles.tabContent}>
|
||||
<img className={styles.icon} src={IndieAuthIcon.src} alt="IndieAuth" />
|
||||
IndieAuth
|
||||
</span>
|
||||
}
|
||||
|
@ -49,8 +47,8 @@ export default function AuthModal(props: Props) {
|
|||
</TabPane>
|
||||
<TabPane
|
||||
tab={
|
||||
<span className={s.tabContent}>
|
||||
<img className={s.icon} src={FediverseIcon.src} alt="Fediverse auth" />
|
||||
<span className={styles.tabContent}>
|
||||
<img className={styles.icon} src={FediverseIcon.src} alt="Fediverse auth" />
|
||||
FediAuth
|
||||
</span>
|
||||
}
|
||||
|
@ -61,4 +59,4 @@ export default function AuthModal(props: Props) {
|
|||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import BrowserNotifyModal from './BrowserNotifyModal';
|
||||
import { BrowserNotifyModal } from './BrowserNotifyModal';
|
||||
import BrowserNotifyModalMock from '../../../stories/assets/mocks/notify-modal.png';
|
||||
|
||||
const Example = () => (
|
||||
|
|
|
@ -1,69 +1,64 @@
|
|||
import { Row, Col, Spin, Typography, Button } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { accessTokenAtom, clientConfigStateAtom } from '../../stores/ClientConfigStore';
|
||||
import {
|
||||
registerWebPushNotifications,
|
||||
saveNotificationRegistration,
|
||||
} from '../../../services/notifications-service';
|
||||
import s from './BrowserNotifyModal.module.scss';
|
||||
import styles from './BrowserNotifyModal.module.scss';
|
||||
import isPushNotificationSupported from '../../../utils/browserPushNotifications';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
function NotificationsNotSupported() {
|
||||
return <div>Browser notifications are not supported in your browser.</div>;
|
||||
}
|
||||
const NotificationsNotSupported = () => (
|
||||
<div>Browser notifications are not supported in your browser.</div>
|
||||
);
|
||||
|
||||
function NotificationsEnabled() {
|
||||
return <div>Notifications enabled</div>;
|
||||
}
|
||||
const NotificationsEnabled = () => <div>Notifications enabled</div>;
|
||||
|
||||
interface PermissionPopupPreviewProps {
|
||||
export type PermissionPopupPreviewProps = {
|
||||
start: () => void;
|
||||
}
|
||||
function PermissionPopupPreview(props: PermissionPopupPreviewProps) {
|
||||
const { start } = props;
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="browser-push-preview-box" className={s.pushPreview}>
|
||||
<div className={s.inner}>
|
||||
<div className={s.title}>{window.location.toString()} wants to</div>
|
||||
<div className={s.permissionLine}>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
fill="#676670"
|
||||
/>
|
||||
</svg>
|
||||
Show notifications
|
||||
</div>
|
||||
<div className={s.buttonRow}>
|
||||
<Button
|
||||
type="primary"
|
||||
className={s.allow}
|
||||
onClick={() => {
|
||||
start();
|
||||
}}
|
||||
>
|
||||
Allow
|
||||
</Button>
|
||||
<button type="button" className={s.disabled}>
|
||||
Block
|
||||
</button>
|
||||
</div>
|
||||
const PermissionPopupPreview: FC<PermissionPopupPreviewProps> = ({ start }) => (
|
||||
<div id="browser-push-preview-box" className={styles.pushPreview}>
|
||||
<div className={styles.inner}>
|
||||
<div className={styles.title}>{window.location.toString()} wants to</div>
|
||||
<div className={styles.permissionLine}>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
fill="#676670"
|
||||
/>
|
||||
</svg>
|
||||
Show notifications
|
||||
</div>
|
||||
<div className={styles.buttonRow}>
|
||||
<Button
|
||||
type="primary"
|
||||
className={styles.allow}
|
||||
onClick={() => {
|
||||
start();
|
||||
}}
|
||||
>
|
||||
Allow
|
||||
</Button>
|
||||
<button type="button" className={styles.disabled}>
|
||||
Block
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function BrowserNotifyModal() {
|
||||
export const BrowserNotifyModal = () => {
|
||||
const [error, setError] = useState<string>(null);
|
||||
const accessToken = useRecoilValue(accessTokenAtom);
|
||||
const config = useRecoilValue(clientConfigStateAtom);
|
||||
|
@ -120,4 +115,4 @@ export default function BrowserNotifyModal() {
|
|||
</Row>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import FatalErrorStateModal from './FatalErrorStateModal';
|
||||
import { FatalErrorStateModal } from './FatalErrorStateModal';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Modals/Global error state',
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
import { Modal } from 'antd';
|
||||
import { FC } from 'react';
|
||||
|
||||
interface Props {
|
||||
export type FatalErrorStateModalProps = {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function FatalErrorStateModal(props: Props) {
|
||||
const { title, message } = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
visible
|
||||
footer={null}
|
||||
closable={false}
|
||||
keyboard={false}
|
||||
width={900}
|
||||
centered
|
||||
className="modal"
|
||||
>
|
||||
<p style={{ fontSize: '1.3rem' }}>{message}</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
export const FatalErrorStateModal: FC<FatalErrorStateModalProps> = ({ title, message }) => (
|
||||
<Modal
|
||||
title={title}
|
||||
visible
|
||||
footer={null}
|
||||
closable={false}
|
||||
keyboard={false}
|
||||
width={900}
|
||||
centered
|
||||
className="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
Loading…
Reference in a new issue