reafctor: normalize component formatting (#2082)

* refactor: move/rename BanUserButton file

* refactor: move/rename Chart file

* refactor: update generic component filenames to PascalCase

* refactor: update config component filenames to PascalCase

* refactor: update AdminLayout component filename to PascalCase

* refactor: update/move VideoJS component

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

* refactor: normalize ActionButton component

* refactor: normalize ActionButtonRow component

* refactor: normalize FollowButton component

* refactor: normalize NotifyButton component

* refactor: normalize ChatActionMessage component

* refactor: normalize ChatContainer component

* refactor: normalize ChatJoinMessage component

* refactor: normalize ChatModerationActionMenu component

* refactor: normalize ChatModerationDetailsModal component

* refactor: normalize ChatModeratorNotification component

* refactor: normalize ChatSocialMessage component

* refactor: normalize ChatSystemMessage component

* refactor: normalize ChatTextField component

* refactor: normalize ChatUserBadge component

* refactor: normalize ChatUserMessage component

* refactor: normalize ContentHeader component

* refactor: normalize OwncastLogo component

* refactor: normalize UserDropdown component

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

* refactor: normalize CodecSelector component

* refactor: update a bunch of functional components using eslint

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

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

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

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

* refactor: remove default exports from most components

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

* fix: use-before-define error in ChatContainer

* Fix ChatContainer import

* Only process .tsx files in Next builds

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

View file

@ -22,7 +22,9 @@ module.exports = {
ignorePatterns: ['!./storybook/**'],
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',

View file

@ -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" />

View file

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

View file

@ -1,15 +1,17 @@
import { Modal, Button } from 'antd';
import { 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,

View file

@ -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[];
}
};

View file

@ -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: [],

View file

@ -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[];
}
};

View file

@ -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,

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { FC, useState } from 'react';
import { Button, Space, Input, Modal } from 'antd';
import { 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>
);
}
};

View file

@ -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[];

View file

@ -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>
);
}
};

View file

@ -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;
}
};

View file

@ -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>
);
}
};

View file

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { FC, ReactNode, useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import 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,

View file

@ -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>
);
}
};

View file

@ -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,
};

View file

@ -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>
);
}
};

View file

@ -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;

View file

@ -3,10 +3,11 @@
// TODO: This component should be cleaned up and usage should be re-examined. The types should be reconsidered as well.
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;

View file

@ -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,

View file

@ -1,25 +1,25 @@
// This displays a clickable user name (or whatever children element you provide), and displays a simple tooltip of created time. OnClick a modal with more information about the user is displayed.
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,

View file

@ -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[];
}
};

View file

@ -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[];
}
};

View file

@ -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',

View file

@ -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({
/>
</>
);
}
};

View file

@ -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',

View file

@ -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>
);

View file

@ -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>
</>
);
}
};

View file

@ -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>
);

View file

@ -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 {

View file

@ -1,13 +1,12 @@
import s from './ChatActionMessage.module.scss';
import { FC } from 'react';
import styles from './ChatActionMessage.module.scss';
/* eslint-disable react/no-danger */
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} />
);

View file

@ -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 {

View file

@ -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,

View file

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

View file

@ -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 {

View file

@ -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>
);
}
};

View file

@ -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"

View file

@ -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>
</>
);
}
};

View file

@ -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>

View file

@ -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>
);
}
};

View file

@ -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',

View file

@ -1,12 +1,9 @@
import s from './ChatModeratorNotification.module.scss';
import styles from './ChatModeratorNotification.module.scss';
import Icon from '../../../assets/images/moderator.svg';
// 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>
);

View file

@ -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',

View file

@ -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>;

View file

@ -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';

View file

@ -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>
);

View file

@ -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(

View file

@ -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>
);
}
};

View file

@ -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} />;
}
};

View file

@ -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',

View file

@ -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>
);
}
};

View file

@ -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';

View file

@ -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>
);
}
};

View file

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

View file

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

View file

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

View file

@ -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}&nbsp;</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}&nbsp;</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>
);

View file

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

View file

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

View file

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

View file

@ -1,15 +1,15 @@
import React from 'react';
import React, { FC } from 'react';
import cn from 'classnames';
import 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>
);
}
};

View file

@ -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',

View file

@ -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,
};

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { Popconfirm, Select, Typography } from 'antd';
import 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>
</>
);
}
};

View file

@ -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>
</>
);
}
};

View file

@ -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>
);
}
};

View file

@ -1,11 +1,10 @@
import React, { useState, useContext, useEffect } from 'react';
import React, { useState, useContext, useEffect, FC } from 'react';
import { Typography } from 'antd';
import 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>
);
}
};

View file

@ -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>
);
}
};

View file

@ -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>
);
}
};

View file

@ -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>
);
}
};

View file

@ -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>
);
}
};

View file

@ -1,8 +1,8 @@
import React, { useState, useContext, useEffect } from 'react';
import React, { useState, useContext, useEffect, FC } from 'react';
import { Typography, Table, Button, Modal, Input } from 'antd';
import { 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>
);
}
};

View file

@ -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>
);
}
};

View file

@ -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,

View file

@ -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>
);
}
};

View file

@ -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;

View file

@ -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>
</>
);
}
};

View file

@ -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>
);
}
};

View file

@ -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: '',

View file

@ -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: '',

View file

@ -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: '',

View file

@ -1,4 +1,4 @@
import React, { useContext, useState, useEffect } from 'react';
import React, { useContext, useState, useEffect, FC } from 'react';
import { Typography, Slider } from 'antd';
import { 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>
);
}
};

View file

@ -1,10 +1,10 @@
// This content populates the video variant modal, which is spawned from the variants table. This relies on the `dataState` prop fed in by the table.
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>
);
}
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

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

View file

@ -1,20 +1,21 @@
import { Layout } from 'antd';
import { 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;
};

View file

@ -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>
);

View file

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

View file

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

View file

@ -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>
);
}
};

View file

@ -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 = () => (

View file

@ -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>
);
}
};

View file

@ -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',

View file

@ -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