Merge branch 'develop' into fix/ImplementPasswordRules

This commit is contained in:
Jambaldorj Ochirpurev
2023-02-05 11:12:05 +01:00
committed by GitHub
871 changed files with 3900 additions and 5007 deletions

View File

@@ -1,3 +1,4 @@
# Ignore artifacts:
node_modules
out
styles/variables.css

View File

@@ -1,16 +1,15 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { ColorRow } from './Color';
<Meta title="owncast/Styles/Colors" />
<Meta title="owncast/Styles/Colors" parameters={{ chromatic: { disableSnapshot: true } }} />
# Default theme colors
These colors are assigned in our [color token](https://github.com/owncast/owncast/tree/webv2/web/style-definitions/tokens/color) files
These colors are assigned in our [color token](https://github.com/owncast/owncast/tree/develop/web/style-definitions/tokens/color) files
and get reflected here as they change. run `npm run build-styles` to regenerate.
<Story
name="Default Theme"
>
<Story name="Default Theme">
## Default Theme
These color names are assigned to specific component variables. They can be overwritten via CSS.

View File

@@ -1,6 +1,6 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/Design" />
<Meta title="owncast/Documentation/Design" parameters={{chromatic: { disableSnapshot: true }}}/>
# Owncast Design Guidelines & Resources

View File

@@ -1,6 +1,6 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/Get Started with Owncast Development" />
<Meta title="owncast/Documentation/Get Started with Owncast Development" parameters={{chromatic: { disableSnapshot: true }}}/>
---
title: "How to work on Owncast"
@@ -35,8 +35,9 @@ The web frontend of Owncast is written in React with TypeScript built using [Nex
You can browse the React components in the project using our [Storybook](https://owncast.online/components) page to get an idea of how the frontend is structured.
1. Clone the Owncast repository with `git clone https://github.com/owncast/owncast`.
1. Change to the `webv2` branch with `git checkout webv2`.
1. Fork the Owncast repository on Github located at https://github.com/owncast/owncast.
1. Check out your fork locally with `git clone https://github.com/yourusername/owncast`.
1. Create a new branch for your new changes with `git checkout -b my-new-feature`.
### Run the web project
@@ -48,7 +49,7 @@ You must have an instance of Owncast running locally to connect to. You can run
### Learn about how to write React Components with Owncast
We have a [short document](https://github.com/owncast/owncast/blob/webv2/web/components/_COMPONENT_HOW_TO.md) outlining the specifics of the hows and whys of our specific component approach.
We have a [short document](https://github.com/owncast/owncast/blob/web/components/_COMPONENT_HOW_TO.md) outlining the specifics of the hows and whys of our specific component approach.
### Use Storybook to update and create components

View File

@@ -1,7 +1,7 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Frontend Assets/Emoji" />
<Meta title="owncast/Frontend Assets/Emoji" parameters={{chromatic: { disableSnapshot: true }}} />
# Built-in Custom Emoji

View File

@@ -1,7 +1,7 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Frontend Assets/Images" />
<Meta title="owncast/Frontend Assets/Images" parameters={{chromatic: { disableSnapshot: true }}} />
# Images

View File

@@ -0,0 +1,147 @@
import { Mermaid } from 'mdx-mermaid/Mermaid';
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta
title="owncast/Documentation/Usage Examples"
parameters={{ chromatic: { disableSnapshot: true } }}
/>
<Story name="Basic">
This is the most basic Owncast setup and is what you get when you run it without any additional services.
<Mermaid
chart={`graph TD
Owncast{fa:fa-server Owncast}
A[fa:fa-window-maximize Web App] --Video-->Owncast
A[fa:fa-window-maximize Web App] --Web-->Owncast
A[fa:fa-window-maximize Web App] <--Chat-->Owncast
B[fa:fa-tv Smart TV] --Video--> Owncast
I[fa:fa-window-restore Chat Embeds] --Web-->Owncast
I[fa:fa-window-restore Chat Embeds] <--Chat-->Owncast
Owncast --Video-->F[fa:fa-shapes VLC]
Owncast --Video-->G[fa:fa-window-restore Video Embeds]
Owncast --Web-->G[fa:fa-window-restore Video Embeds]
Owncast --Video-->H[fa:fa-mobile-screen Mobile Apps]
`}
/>
</Story>
<Story name="S3 Object Storage">
If you would like no video traffic to take place from your Owncast server to your clients, you can use S3 Object Storage to be responsible for this instead.
Instead of video players pulling the stream from your Owncast server it would instead make these requests to a S3 provider, and Owncast would upload video to this provider to make it available.
<Mermaid
chart={`graph TD
subgraph Chat & Web Assets
Owncast{fa:fa-server Owncast}
end
Owncast--Upload\\nVideo-->S3
subgraph Video Assets
S3[fa:fa-hard-drive S3 Object Storage]
end
subgraph Clients
WebPlayer[fa:fa-window-maximize Web App]
Embeds[fa:fa-window-restore Embeds]
MobileApps[fa:fa-mobile-screen Mobile Apps]
SmartTV[fa:fa-tv Smart TV]
VLC[fa:fa-shapes VLC]
end
Owncast--Web-->WebPlayer
Owncast--Web-->Embeds
Owncast<--Chat-->WebPlayer
S3--Download\\nVideo-->WebPlayer
S3--Download\\nVideo-->Embeds
S3--Download\\nVideo-->MobileApps
S3--Download\\nVideo-->SmartTV
S3--Download\\nVideo-->VLC
`}
/>
</Story>
<Story name="CDN in front of S3 Object Storage">
If you're using a S3 provider but would like to increase the speed of your video delivery to your clients around the world, you can use a CDN in front of your S3 provider.
This leads to not only your server not serving any video traffic, but the video traffic would be distributed globally so each client player would be pulling video from somewhere geographically closer, reducing the likelihood of buffering and slower network requests.
<Mermaid
chart={`flowchart TD
subgraph Video Assets
S3[fa:fa-hard-drive S3 Object Storage]
CDN[fa:fa-cloud Global CDN]
end
subgraph Clients
WebPlayer[fa:fa-window-maximize Web App]
Embeds[fa:fa-window-restore Embeds]
MobileApps[fa:fa-mobile-screen Mobile Apps]
SmartTV[fa:fa-tv Smart TV]
VLC[fa:fa-shapes VLC]
end
subgraph Web Assets & Chat Service
direction TB
Owncast{fa:fa-server Owncast}
end
Owncast--Upload\\nVideo-->S3
Owncast--Web-->WebPlayer
Owncast<--Chat-->WebPlayer
Owncast--Web-->Embeds
CDN--Download\\nVideo-->WebPlayer
CDN--Download\\nVideo-->Embeds
CDN--Download\\nVideo-->MobileApps
CDN--Download\\nVideo-->SmartTV
CDN--Download\\nVideo-->VLC
S3 --- CDN
`}
/>
</Story>
<Story name="CDN in front of Owncast">
If you're ok with some video requests coming directly to your Owncast server, and want to generally increase the overall speed that your viewers globally access all your Owncast assets, including video and the web interface, you can put a CDN in front of your entire Owncast server.
<Mermaid
chart={`flowchart TD
CDN{{fa:fa-cloud Global CDN}}
Owncast{fa:fa-server Owncast}
subgraph Clients
WebPlayer[fa:fa-window-maximize Web App]
Embeds[fa:fa-window-restore Embeds]
MobileApps[fa:fa-mobile-screen Mobile Apps]
SmartTV[fa:fa-tv Smart TV]
VLC[fa:fa-shapes VLC]
end
CDN--Web-->WebPlayer
Owncast<--Chat-->WebPlayer
CDN--Web-->Embeds
CDN--Download\\nVideo-->WebPlayer
CDN--Download\\nVideo-->Embeds
CDN--Download\\nVideo-->MobileApps
CDN--Download\\nVideo-->SmartTV
CDN--Download\\nVideo-->VLC
CDN --- Owncast
`}
/>
</Story>

View File

@@ -1,69 +0,0 @@
import { Meta } from '@storybook/addon-docs';
import { Typography } from 'antd';
<Meta title="Owncast/Documentation/Readme" />
<Typography.Title style={{ color: 'var(--primary-color)' }}>Owncast Web UI v2</Typography.Title>
Owncast is going through a complete rewrite of the web app frontend.
Visit the [UIv2 milestone](https://github.com/owncast/owncast/milestone/18) on GitHub to see the individual tasks for this project.
## Quick Links
- [Redesign project](https://github.com/owncast/owncast/milestone/18)
- [Currently defined colors](/story/owncast-style-guide-colors--page)
- [Owncast Frontend Chat](https://owncast.rocket.chat/group/frontend-dev)
## Why?
- Moving to a full React Component workflow allows the project to be more productive and build features faster.
- Share code between the web frontend UI and the existing admin.
- Address feedback from users.
- Better accessibility.
- Better mobile experience.
- Standardize styling across the project by using a single design language and style guide.
- Allows more people to contribute to the project if we use popular frameworks.
## What
- [Next.js](https://nextjs.org/)
- [React](https://reactjs.org/)
- [Ant Design](https://ant.design/)
## Contributing
1. Find a component that hasn't yet been worked on by looking through the [UIv2 milestone](https://github.com/owncast/owncast/milestone/18)
and the sidebar of components to the left.
1. See if you can have an example of this functionality in action via the [Owncast Demo Server](https://watch.owncast.online) or [Owncast Nightly Build](https://nightly.owncast.online) so you know how it's supposed to work if it's interactive.
1. Visit the `Docs` tab to read any specific documentation that may have been written about how this component works.
1. Go to the `Canvas` tab of the component you selected and see if there's a Design attached to it.
1. If there is a design, then that's a starting point you can use to start building out the component.
1. If there isn't, then visit the [Owncast Demo Server](https://watch.owncast.online), the [Owncast Nightly Build](https://nightly.owncast.online), or the proposed [v2 design](https://www.figma.com/proto/B6ICOn1J3dyYeoZM5kPM2A/Owncast---Review?node-id=643%3A646&scaling=min-zoom&page-id=643%3A17&starting-point-node-id=643%3A44) for some ways to start.
1. If no design exists, then you can ask around the Owncast chat for help, for come up with your own ideas!
1. No designs are stuck in stone, and we're using this as an opportunity to level up the UI of Owncast, so all ideas are welcome.
## How?
This rewrite is a large project, but like anything else, breaking it into pieces and working on one thing at a time will eventually get us to the finish line.
And that's what this interface lets us do. On this page we see all the different components still needing to be worked on, and have a place to document the functionality of these pieces.
## What about the Admin?
The admin has always been a Next+React+Ant project, so the goal is to touch that as little as possible except where needed to share code and styles.
## What is this page?
This is called [_Storybook_](https://storybook.js.org/docs/react/get-started/introduction).
Storybook is a tool for UI development. It makes development faster and easier by isolating components.
This allows you to work on one component at a time. You can develop entire UIs without needing to start
up a complex dev stack, force certain data into your database, or navigate around your application.
For example a button may have a disabled state that requires a specific scenario to take place in real-world use,
but here we you can just toggle the state to verify things are working as expected.
This means [new components should have a corresponding story added](https://storybook.js.org/docs/react/get-started/whats-a-story) to make it easier to maintain the project.
For more details about building React components read [this document](https://github.com/owncast/owncast/blob/webv2/web/components/_COMPONENT_HOW_TO.md) with specifics.
<img src="https://user-images.githubusercontent.com/9563255/187112334-e6c73bad-72df-42fc-9ee2-8d36ba615275.png"/>

View File

@@ -1,7 +1,7 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Project Assets/Logos &amp; Graphics" />
<Meta title="owncast/Project Assets/Logos &amp; Graphics" parameters={{chromatic: { disableSnapshot: true }}} />
# Logos &amp; Graphics

View File

@@ -1,6 +1,6 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/Product Definition" />
<Meta title="owncast/Documentation/Product Definition" parameters={{chromatic: { disableSnapshot: true }}}/>
# Owncast Product Definition

View File

@@ -1,7 +1,7 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Frontend Assets/Social Platform Images" />
<Meta title="owncast/Frontend Assets/Social Platform Images" parameters={{chromatic: { disableSnapshot: true }}} />
# Social Platform Images

View File

@@ -1,7 +1,7 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Project Assets/T-Shirt" />
<Meta title="owncast/Project Assets/T-Shirt" parameters={{chromatic: { disableSnapshot: true }}} />
# T-shirt

View File

@@ -1,6 +1,6 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/Building Frontend Components" />
<Meta title="owncast/Documentation/Building Frontend Components" parameters={{chromatic: { disableSnapshot: true }}}/>
# How we develop components

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 121 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 KiB

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 KiB

After

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 KiB

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 KiB

After

Width:  |  Height:  |  Size: 683 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 KiB

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 KiB

After

Width:  |  Height:  |  Size: 741 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 KiB

After

Width:  |  Height:  |  Size: 456 KiB

View File

@@ -1,5 +1,5 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/{{title}}" />
<Meta title="owncast/Documentation/{{title}}" parameters=\{{chromatic: { disableSnapshot: true }}}/>
{{content}}

View File

@@ -1,7 +1,7 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Frontend Assets/Emoji" />
<Meta title="owncast/Frontend Assets/Emoji" parameters=\{{chromatic: { disableSnapshot: true }}} />
# Built-in Custom Emoji

View File

@@ -1,7 +1,7 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="{{category}}" />
<Meta title="{{category}}" parameters=\{{chromatic: { disableSnapshot: true }}} />
# {{capitalize title}}

View File

@@ -1,15 +1,35 @@
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);
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
LogarithmicScale,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
ChartJS.register(
CategoryScale,
LogarithmicScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
);
interface TimedValue {
time: Date;
value: number;
pointStyle?: boolean | string;
pointRadius?: number;
}
export type ChartProps = {
@@ -45,47 +65,46 @@ export const Chart: FC<ChartProps> = ({
if (data && data.length > 0) {
renderData.push({
name: title,
color,
id: title,
label: title,
backgroundColor: color,
borderColor: color,
borderWidth: 3,
data: createGraphDataset(data),
});
}
dataCollections.forEach(collection => {
renderData.push({
name: collection.name,
id: collection.name,
label: collection.name,
data: createGraphDataset(collection.data),
color: collection.color,
dataset: collection.options,
backgroundColor: collection.color,
borderColor: collection.color,
borderWidth: 3,
pointStyle: collection.pointStyle || 'circle',
radius: collection.pointRadius || 1,
});
});
// ChartJs.defaults.scales.linear.reverse = true;
const options = {
responsive: true,
scales: {
y: { reverse: false, type: 'linear' },
x: {
type: 'time',
y: {
type: yLogarithmic ? ('logarithmic' as const) : ('linear' as const),
reverse: yFlipped,
title: {
display: true,
text: unit,
},
},
},
};
options.scales.y.reverse = yFlipped;
options.scales.y.type = yLogarithmic ? 'logarithmic' : 'linear';
return (
<div className="line-chart-container">
<LineChart
xtitle="Time"
ytitle={title}
suffix={unit}
legend="bottom"
color={color}
data={renderData}
download={title}
library={options}
/>
<Line data={{ datasets: renderData }} options={options} height="70vh" />
</div>
);
};

View File

@@ -117,7 +117,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
content: (
<div>
List yourself in the Owncast Directory and show off your stream. Enable it in{' '}
<Link href="/config-public-details">settings.</Link>
<Link href="/admin/config/general/">settings.</Link>
</div>
),
});
@@ -129,7 +129,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
title: 'Add your Owncast instance to the Fediverse',
content: (
<div>
<Link href="/config-federation">Enable Owncast social</Link> features to have your
<Link href="/admin/config-federation/">Enable Owncast social</Link> features to have your
instance join the Fediverse, allowing people to follow, share and engage with your live
stream.
</div>

View File

@@ -33,7 +33,7 @@ export const ConfigNotify = () => {
</span>
</p>
<Link passHref href="/config-federation">
<Link passHref href="/admin/config-federation/">
<Button
type="primary"
style={{

View File

@@ -8,7 +8,7 @@ export default {
title: 'owncast/Chat/Chat messages container',
component: ChatContainer,
parameters: {
chromatic: { diffThreshold: 0.2 },
chromatic: { diffThreshold: 0.8 },
docs: {
description: {
component: `

View File

@@ -1,7 +1,4 @@
.root {
padding: 10px 0px;
text-align: center;
font-size: 0.8rem;
font-style: italic;
color: var(--theme-color-components-chat-text);
}

View File

@@ -1,7 +1,7 @@
import { FC } from 'react';
import dynamic from 'next/dynamic';
import { ChatUserBadge } from '../ChatUserBadge/ChatUserBadge';
import styles from './ChatJoinMessage.module.scss';
import { ModerationBadge } from '../ChatUserBadge/ModerationBadge';
// Lazy loaded components
@@ -31,7 +31,7 @@ export const ChatJoinMessage: FC<ChatJoinMessageProps> = ({
<span style={{ fontWeight: 'bold' }}>{displayName}</span>
{isAuthorModerator && (
<span>
<ChatUserBadge badge="mod" userColor={userColor} />
<ModerationBadge userColor={userColor} />
</span>
)}
</span>{' '}

View File

@@ -33,7 +33,7 @@ export default {
component: ChatTextField,
parameters: {
fetchMock: mocks,
chromatic: { diffThreshold: 0.2 },
chromatic: { diffThreshold: 0.8 },
design: {
type: 'image',

View File

@@ -0,0 +1,17 @@
import dynamic from 'next/dynamic';
import React, { FC } from 'react';
import { ChatUserBadge } from './ChatUserBadge';
// Lazy loaded components
const SafetyCertificateFilled = dynamic(() => import('@ant-design/icons/SafetyCertificateFilled'), {
ssr: false,
});
export type AuthedUserBadgeProps = {
userColor: number;
};
export const AuthedUserBadge: FC<AuthedUserBadgeProps> = ({ userColor }) => (
<ChatUserBadge badge={<SafetyCertificateFilled />} userColor={userColor} title="Authenticated" />
);

View File

@@ -1,13 +1,15 @@
.badge {
font-family: var(--theme-text-display-font-family);
font-weight: 500;
font-size: 0.5rem;
text-transform: uppercase;
color: white;
background-color: var(--color-owncast-palette-0);
height: 18px;
width: 18px;
border-radius: 2px;
text-align: center;
padding: 2px;
padding-top: 0px;
padding-bottom: 0px;
border-radius: 3px;
border-width: 1px;
border-style: solid;
margin-left: 3px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
margin-left: 5px;
font-size: 0.75rem;
}

View File

@@ -1,6 +1,8 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { ChatUserBadge } from './ChatUserBadge';
import { ModerationBadge } from './ModerationBadge';
import { AuthedUserBadge } from './AuthedUserBadge';
export default {
title: 'owncast/Chat/Messages/User Flag',
@@ -14,15 +16,26 @@ export default {
} as ComponentMeta<typeof ChatUserBadge>;
const Template: ComponentStory<typeof ChatUserBadge> = args => <ChatUserBadge {...args} />;
const ModerationTemplate: ComponentStory<typeof ModerationBadge> = args => (
<ModerationBadge {...args} />
);
export const Moderator = Template.bind({});
const AuthedTemplate: ComponentStory<typeof ModerationBadge> = args => (
<AuthedUserBadge {...args} />
);
export const Authenticated = AuthedTemplate.bind({});
Authenticated.args = {
userColor: '3',
};
export const Moderator = ModerationTemplate.bind({});
Moderator.args = {
badge: 'mod',
userColor: '5',
};
export const Authenticated = Template.bind({});
Authenticated.args = {
badge: 'auth',
export const Generic = Template.bind({});
Generic.args = {
badge: '?',
userColor: '6',
};

View File

@@ -4,14 +4,15 @@ import styles from './ChatUserBadge.module.scss';
export type ChatUserBadgeProps = {
badge: React.ReactNode;
userColor: number;
title: string;
};
export const ChatUserBadge: FC<ChatUserBadgeProps> = ({ badge, userColor }) => {
const color = `var(--theme-user-colors-${userColor})`;
const style = { color, borderColor: color };
export const ChatUserBadge: FC<ChatUserBadgeProps> = ({ badge, userColor, title }) => {
const color = `var(--theme-color-users-${userColor})`;
const style = { color };
return (
<span style={style} className={styles.badge}>
<span style={style} className={styles.badge} title={title}>
{badge}
</span>
);

View File

@@ -0,0 +1,17 @@
import dynamic from 'next/dynamic';
import React, { FC } from 'react';
import { ChatUserBadge } from './ChatUserBadge';
// Lazy loaded components
const StarFilled = dynamic(() => import('@ant-design/icons/StarFilled'), {
ssr: false,
});
export type ModerationBadgeProps = {
userColor: number;
};
export const ModerationBadge: FC<ModerationBadgeProps> = ({ userColor }) => (
<ChatUserBadge badge={<StarFilled />} userColor={userColor} title="Moderator" />
);

View File

@@ -25,9 +25,18 @@ $p-size: 8px;
position: relative;
mark {
padding-left: 0.35em;
padding-right: 0.35em;
padding-left: 0.3em;
padding-right: 0.3em;
color: var(--theme-color-palette-4);
border-radius: var(--theme-rounded-corners);
background-color: var(--color-owncast-palette-7);
}
a {
color: var(--theme-color-palette-12);
&:hover {
color: var(--theme-color-palette-4);
}
}
}

View File

@@ -43,6 +43,21 @@ const standardMessage: ChatMessage = JSON.parse(`{
},
"body": "Test message from a regular user."}`);
const messageWithLinkAndCustomEmoji: ChatMessage = JSON.parse(`{
"type": "CHAT",
"id": "wY-MEXwnR",
"timestamp": "2022-04-28T20:30:27.001762726Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 3,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": ["gifted-nobel", "EliteMooseTaskForce"],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": []
},
"body": "Test message with a link https://owncast.online and a custom emoji <img src='/img/emoji/blob/ablobattention.gif' width='30px'/> ."}`);
const moderatorMessage: ChatMessage = JSON.parse(`{
"type": "CHAT",
"id": "wY-MEXwnR",
@@ -80,6 +95,12 @@ WithoutModeratorMenu.args = {
showModeratorMenu: false,
};
export const WithLinkAndCustomEmoji = Template.bind({});
WithLinkAndCustomEmoji.args = {
message: messageWithLinkAndCustomEmoji,
showModeratorMenu: false,
};
export const WithModeratorMenu = Template.bind({});
WithModeratorMenu.args = {
message: standardMessage,

View File

@@ -1,24 +1,22 @@
/* eslint-disable react/no-danger */
import { FC, ReactNode, useEffect, useState } from 'react';
import { FC, ReactNode } from 'react';
import cn from 'classnames';
import { Tooltip } from 'antd';
import { useRecoilValue } from 'recoil';
import dynamic from 'next/dynamic';
import { decodeHTML } from 'entities';
import linkifyHtml from 'linkify-html';
import { Interweave } from 'interweave';
import { UrlMatcher } from 'interweave-autolink';
import { ChatMessageHighlightMatcher } from './customMatcher';
import styles from './ChatUserMessage.module.scss';
import { formatTimestamp } from './messageFmt';
import { ChatMessage } from '../../../interfaces/chat-message.model';
import { ChatUserBadge } from '../ChatUserBadge/ChatUserBadge';
import { accessTokenAtom } from '../../stores/ClientConfigStore';
import { User } from '../../../interfaces/user.model';
import { AuthedUserBadge } from '../ChatUserBadge/AuthedUserBadge';
import { ModerationBadge } from '../ChatUserBadge/ModerationBadge';
// Lazy loaded components
const LinkOutlined = dynamic(() => import('@ant-design/icons/LinkOutlined'), {
ssr: false,
});
const ChatModerationActionMenu = dynamic(
() =>
import('../ChatModerationActionMenu/ChatModerationActionMenu').then(
@@ -29,10 +27,6 @@ const ChatModerationActionMenu = dynamic(
},
);
const Highlight = dynamic(() => import('react-highlighter-ts').then(mod => mod.Highlight), {
ssr: false,
});
export type ChatUserMessageProps = {
message: ChatMessage;
showModeratorMenu: boolean;
@@ -74,26 +68,15 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
const color = `var(--theme-color-users-${displayColor})`;
const formattedTimestamp = `Sent ${formatTimestamp(timestamp)}`;
const [formattedMessage, setFormattedMessage] = useState<string>(body);
const badgeNodes = [];
if (isAuthorModerator) {
badgeNodes.push(<ChatUserBadge key="mod" badge="mod" userColor={displayColor} />);
badgeNodes.push(<ModerationBadge key="mod" userColor={displayColor} />);
}
if (isAuthorAuthenticated) {
badgeNodes.push(
<ChatUserBadge
key="auth"
badge={<LinkOutlined title="authenticated" />}
userColor={displayColor}
/>,
);
badgeNodes.push(<AuthedUserBadge key="auth" userColor={displayColor} />);
}
useEffect(() => {
setFormattedMessage(decodeHTML(body));
}, [message]);
return (
<div
className={cn(
@@ -119,12 +102,14 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
</UserTooltip>
)}
<Tooltip title={formattedTimestamp} mouseEnterDelay={1}>
<Highlight search={highlightString}>
<div
className={styles.message}
dangerouslySetInnerHTML={{ __html: linkifyHtml(formattedMessage) }}
/>
</Highlight>
<Interweave
className={styles.message}
content={body}
matchers={[
new UrlMatcher('url', { validateTLD: false }),
new ChatMessageHighlightMatcher('highlight', { highlightString }),
]}
/>
</Tooltip>
{showModeratorMenu && (
<div className={styles.modMenuWrapper}>

View File

@@ -0,0 +1,44 @@
/* eslint-disable class-methods-use-this */
import { ChildrenNode, Matcher, MatchResponse, Node } from 'interweave';
import React from 'react';
export interface CustomProps {
children: React.ReactNode;
key: string;
}
interface options {
highlightString: string;
}
export class ChatMessageHighlightMatcher extends Matcher {
match(str: string): MatchResponse<{}> | null {
const { highlightString } = this.options as options;
if (!highlightString) {
return null;
}
const result = str.match(highlightString);
if (!result) {
return null;
}
return {
index: result.index!,
length: result[0].length,
match: result[0],
valid: true,
};
}
replaceWith(children: ChildrenNode, props: CustomProps): Node {
const { key } = props;
return React.createElement('mark', { key }, children);
}
asTag(): string {
return 'mark';
}
}

View File

@@ -1,38 +1,8 @@
import { convertToText } from '../chat';
import { getDiffInDaysFromNow } from '../../../utils/helpers';
const stripTags = (str: string) => str && str.replace(/<\/?[^>]+(>|$)/g, '');
const convertToMarkup = (str = '') => convertToText(str).replace(/\n/g, '<p></p>');
function getInstagramEmbedFromURL(url: string) {
const urlObject = new URL(url.replace(/\/$/, ''));
urlObject.pathname += '/embed';
return `<iframe class="chat-embed instagram-embed" src="${urlObject.href}" frameborder="0" allowfullscreen></iframe>`;
}
function isMessageJustAnchor(embedText: string, message: string, anchors: HTMLAnchorElement[]) {
if (embedText !== '' && anchors.length === 1) return false;
return stripTags(message) === stripTags(anchors[0]?.innerHTML);
}
function getMessageWithEmbeds(message: string) {
let embedText = '';
// Make a temporary element so we can actually parse the html and pull anchor tags from it.
// This is a better approach than regex.
const container = document.createElement('p');
container.innerHTML = message;
const anchors = Array.from(container.querySelectorAll('a'));
anchors.forEach(({ href }) => {
if (href.includes('instagram.com/p/')) embedText += getInstagramEmbedFromURL(href);
});
// If this message only consists of a single embeddable link
// then only return the embed and strip the link url from the text.
if (isMessageJustAnchor(embedText, message, anchors)) return embedText;
return message + embedText;
}
export function formatTimestamp(sentAt: Date) {
const now = new Date(sentAt);
if (Number.isNaN(now)) return '';
@@ -56,8 +26,6 @@ export function formatTimestamp(sentAt: Date) {
*/
export function formatMessageText(message: string) {
let formattedText = getMessageWithEmbeds(message);
formattedText = convertToMarkup(formattedText);
const formattedText = convertToMarkup(message);
return formattedText;
// return await highlightUsername(formattedText, username);
}

View File

@@ -3,17 +3,18 @@ import cn from 'classnames';
import styles from './OwncastLogo.module.scss';
export type LogoProps = {
variant: 'simple' | 'contrast';
variant?: 'simple' | 'contrast';
className?: string;
};
export const OwncastLogo: FC<LogoProps> = ({ variant = 'simple' }) => {
export const OwncastLogo: FC<LogoProps> = ({ variant = 'simple', className = '' }) => {
const rootClassName = cn(styles.root, {
[styles.simple]: variant === 'simple',
[styles.contrast]: variant === 'contrast',
});
return (
<div className={rootClassName}>
<div className={`${rootClassName} ${className}`}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 95.68623352050781 104.46271514892578"

View File

@@ -24,6 +24,7 @@ import { Theme } from '../../theme/Theme';
import styles from './Main.module.scss';
import { PushNotificationServiceWorker } from '../../workers/PushNotificationServiceWorker/PushNotificationServiceWorker';
import { AppStateOptions } from '../../stores/application-state';
import { Noscript } from '../../ui/Noscript/Noscript';
const lockBodyStyle = `
body {
@@ -152,6 +153,8 @@ export const Main: FC = () => {
<FatalErrorStateModal title={fatalError.title} message={fatalError.message} />
)}
</Layout>
<Noscript />
</>
);
};

View File

@@ -14,6 +14,7 @@ export default {
title: 'owncast/Modals/Browser Notifications',
component: BrowserNotifyModal,
parameters: {
chromatic: { diffThreshold: 0.7 },
design: {
type: 'image',
url: BrowserNotifyModalMock,

View File

@@ -158,7 +158,7 @@ export const ClientConfigStore: FC = () => {
const [currentUser, setCurrentUser] = useRecoilState(currentUserAtom);
const setChatAuthenticated = useSetRecoilState<boolean>(chatAuthenticatedAtom);
const [clientConfig, setClientConfig] = useRecoilState<ClientConfig>(clientConfigStateAtom);
const [serverStatus, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState);
const [, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState);
const setClockSkew = useSetRecoilState<Number>(clockSkewAtom);
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
@@ -166,7 +166,6 @@ export const ClientConfigStore: FC = () => {
const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom);
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
const [hiddenMessageIds, setHiddenMessageIds] = useRecoilState<string[]>(removedMessageIdsAtom);
const [, setHasLoadedStatus] = useState(false);
const [hasLoadedConfig, setHasLoadedConfig] = useState(false);
let ws: WebsocketService;
@@ -177,21 +176,27 @@ export const ClientConfigStore: FC = () => {
message,
});
};
const sendEvent = (event: string) => {
const sendEvent = (events: string[]) => {
// console.debug('---- sending event:', event);
appStateSend({ type: event });
appStateSend(events);
};
const handleStatusChange = (status: ServerStatus) => {
if (appState.matches('loading')) {
sendEvent(AppStateEvent.Loaded);
const events = [AppStateEvent.Loaded];
if (status.online) {
events.push(AppStateEvent.Online);
} else {
events.push(AppStateEvent.Offline);
}
sendEvent(events);
return;
}
if (status.online && appState.matches('ready')) {
sendEvent(AppStateEvent.Online);
sendEvent([AppStateEvent.Online]);
} else if (!status.online && !appState.matches('ready.offline')) {
sendEvent(AppStateEvent.Offline);
sendEvent([AppStateEvent.Offline]);
}
};
@@ -210,8 +215,9 @@ export const ClientConfigStore: FC = () => {
const updateServerStatus = async () => {
try {
const status = await ServerStatusService.getStatus();
handleStatusChange(status);
setServerStatus(status);
setHasLoadedStatus(true);
const { serverTime } = status;
const clockSkew = new Date(serverTime).getTime() - Date.now();
@@ -219,7 +225,7 @@ export const ClientConfigStore: FC = () => {
setGlobalFatalErrorMessage(null);
} catch (error) {
sendEvent(AppStateEvent.Fail);
sendEvent([AppStateEvent.Fail]);
setGlobalFatalError('Unable to reach Owncast server', serverConnectivityError);
console.error(`serverStatusState -> getStatus() ERROR: \n${error}`);
}
@@ -233,7 +239,7 @@ export const ClientConfigStore: FC = () => {
}
try {
sendEvent(AppStateEvent.NeedsRegister);
sendEvent([AppStateEvent.NeedsRegister]);
const response = await ChatService.registerUser(optionalDisplayName);
const { accessToken: newAccessToken, displayName: newDisplayName, displayColor } = response;
if (!newAccessToken) {
@@ -248,7 +254,7 @@ export const ClientConfigStore: FC = () => {
setAccessToken(newAccessToken);
setLocalStorage(ACCESS_TOKEN_KEY, newAccessToken);
} catch (e) {
sendEvent(AppStateEvent.Fail);
sendEvent([AppStateEvent.Fail]);
console.error(`ChatService -> registerUser() ERROR: \n${e}`);
}
};
@@ -356,17 +362,13 @@ export const ClientConfigStore: FC = () => {
if ((window as any).statusHydration) {
const status = JSON.parse((window as any).statusHydration);
setServerStatus(status);
setHasLoadedStatus(true);
handleStatusChange(status);
}
} catch (e) {
console.error('error parsing status hydration', e);
}
}, []);
useEffect(() => {
handleStatusChange(serverStatus);
}, [serverStatus]);
useEffect(() => {
if (!clientConfig.chatDisabled && accessToken && hasLoadedConfig) {
startChat();

View File

@@ -77,6 +77,7 @@ const OwncastPlayer = dynamic(
() => import('../../video/OwncastPlayer/OwncastPlayer').then(mod => mod.OwncastPlayer),
{
ssr: false,
loading: () => <Skeleton loading active paragraph={{ rows: 12 }} />,
},
);
@@ -103,7 +104,7 @@ const DesktopContent = ({
}) => {
const aboutTabContent = <CustomPageContent content={extraPageContent} />;
const followersTabContent = (
<div style={{ minHeight: '16vh' }}>
<div>
<FollowerCollection name={name} onFollowButtonClick={() => setShowFollowModal(true)} />
</div>
);
@@ -343,16 +344,18 @@ export const Content: FC = () => {
/>
)}
{!online && !appState.appLoading && (
<OfflineBanner
showsHeader={false}
streamName={name}
customText={offlineMessage}
notificationsEnabled={browserNotificationsEnabled}
fediverseAccount={fediverseAccount}
lastLive={lastDisconnectTime}
onNotifyClick={() => setShowNotifyModal(true)}
onFollowClick={() => setShowFollowModal(true)}
/>
<div id="offline-message">
<OfflineBanner
showsHeader={false}
streamName={name}
customText={offlineMessage}
notificationsEnabled={browserNotificationsEnabled}
fediverseAccount={fediverseAccount}
lastLive={lastDisconnectTime}
onNotifyClick={() => setShowNotifyModal(true)}
onFollowClick={() => setShowFollowModal(true)}
/>
</div>
)}
{isStreamLive && (
<Statusbar

View File

@@ -6,7 +6,9 @@ import { Header } from './Header';
export default {
title: 'owncast/Layout/Header',
component: Header,
parameters: {},
parameters: {
chromatic: { diffThreshold: 0.75 },
},
} as ComponentMeta<typeof Header>;
const Template: ComponentStory<typeof Header> = args => (

View File

@@ -29,10 +29,14 @@ export const Header: FC<HeaderComponentProps> = ({
online,
}) => (
<header className={cn([`${styles.header}`], 'global-header')}>
{online && (
{online ? (
<Link href="#player" className={styles.skipLink}>
Skip to player
</Link>
) : (
<Link href="#offline-message" className={styles.skipLink}>
Skip to offline message
</Link>
)}
<Link href="#skip-to-content" className={styles.skipLink}>
Skip to page content

View File

@@ -24,7 +24,7 @@
.image {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
object-fit: cover;
object-position: center;
overflow: hidden;
}

View File

@@ -0,0 +1,50 @@
@import '../../../styles/mixins.scss';
.noscript {
position: fixed;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 2em;
font-size: large;
background-color: var(--theme-color-background-main);
z-index: 999;
h2 {
margin-top: 25px;
margin-bottom: 5px;
font-weight: bold;
font-size: inherit;
}
}
// Necessary in case content y-overflows becuase
// align-items: center would otherwise hide some
// of the content
.scrollContainer {
max-height: 100%;
overflow: auto;
}
.content {
max-width: 100%;
width: 70ch;
display: flex;
flex-direction: column;
align-items: center;
}
.logo {
width: 70%;
// For some weir reason, the logo isn't displayed on screens <= 767px.
// This coincides with the tablet breakpoint, but god knows what exactly
// the issue is. Since it's just a design element, just hide the logo on
// those smaller screens. For more information, see
// https://github.com/owncast/owncast/pull/2592
@include screen(tablet) {
display: none;
}
}

View File

@@ -0,0 +1,43 @@
import { FC } from 'react';
import { OwncastLogo } from '../../common/OwncastLogo/OwncastLogo';
import styles from './Noscript.module.scss';
export const Noscript: FC = () => (
<noscript className={styles.noscript}>
<div className={styles.scrollContainer}>
<div className={styles.content}>
<OwncastLogo className={styles.logo} />
<br />
<p>
This website is powered by&nbsp;
<a href="https://owncast.online" rel="noopener noreferrer" target="_blank">
Owncast
</a>
.
</p>
<p>
Owncast uses JavaScript for playing the HTTP Live Streaming (HLS) video, and its chat
client. But your web browser does not seem to support JavaScript, or you have it disabled.
</p>
<p>
For the best experience, you should use a different browser with JavaScript support. If
you have disabled JavaScript in your browser, you can re-enable it.
</p>
<h2>How can I watch this stream without JavaScript?</h2>
<p>
You can open the URL of this website in your media player (such as&nbsp;
<a href="https://mpv.io" rel="noopener noreferrer" target="_blank">
mpv
</a>
&nbsp;or&nbsp;
<a href="https://www.videolan.org/vlc/" rel="noopener noreferrer" target="_blank">
VLC
</a>
) to watch the stream.
</p>
<h2>How can I chat with the others without JavaScript?</h2>
<p>Currently, there is no option to use the chat without JavaScript.</p>
</div>
</div>
</noscript>
);

View File

@@ -1,3 +1,5 @@
@import '../../../styles/mixins.scss';
.outerContainer {
display: flex;
justify-content: center;
@@ -11,9 +13,16 @@
background-color: var(--theme-color-background-main);
margin: 3rem auto;
border-radius: var(--theme-rounded-corners);
padding: 2.5em;
font-size: 1.2rem;
padding: 2.4em;
font-size: 1.3rem;
border: 1px solid lightgray;
font-family: var(--theme-text-display-font-family);
@include screen(tablet) {
font-size: 1.2rem;
padding: 1em;
margin: 1rem auto;
}
}
.bodyText {
@@ -29,6 +38,7 @@
margin-top: 15px;
font-size: 1rem;
opacity: 0.5;
font-family: var(--theme-text-body-font-family);
.clockIcon {
margin-right: 5px;

View File

@@ -3,6 +3,7 @@ import intervalToDuration from 'date-fns/intervalToDuration';
import { FC, useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import styles from './Statusbar.module.scss';
import { pluralize } from '../../../utils/helpers';
// Lazy loaded components
@@ -18,15 +19,23 @@ export type StatusbarProps = {
};
function makeDurationString(lastConnectTime: Date): string {
const DAY_LABEL = 'day';
const HOUR_LABEL = 'hour';
const MINUTE_LABEL = 'minute';
const SECOND_LABEL = 'second';
const diff = intervalToDuration({ start: lastConnectTime, end: new Date() });
if (diff.days > 1) {
return `${diff.days} days ${diff.hours} hours`;
if (diff.days >= 1) {
return `${diff.days} ${pluralize(DAY_LABEL, diff.days)}
${diff.hours} ${pluralize(HOUR_LABEL, diff.hours)}`;
}
if (diff.hours >= 1) {
return `${diff.hours} hours ${diff.minutes} minutes`;
return `${diff.hours} ${pluralize(HOUR_LABEL, diff.hours)} ${diff.minutes}
${pluralize(MINUTE_LABEL, diff.minutes)}`;
}
return `${diff.minutes} minutes ${diff.seconds} seconds`;
return `${diff.minutes} ${pluralize(MINUTE_LABEL, diff.minutes)}
${diff.seconds} ${pluralize(SECOND_LABEL, diff.seconds)}`;
}
export const Statusbar: FC<StatusbarProps> = ({

View File

@@ -27,7 +27,7 @@
.account {
color: var(--theme-color-components-text-on-light);
word-break: break-all;
line-height: 0.9rem;
line-height: 1rem;
}
@include screen(mobile) {

View File

@@ -1,4 +1,4 @@
import { Avatar, Col, Row } from 'antd';
import { Avatar, Col, Row, Typography } from 'antd';
import React, { FC } from 'react';
import cn from 'classnames';
import { Follower } from '../../../../interfaces/follower';
@@ -17,9 +17,13 @@ export const SingleFollower: FC<SingleFollowerProps> = ({ follower }) => (
<img src="/logo" alt="Logo" className={styles.placeholder} />
</Avatar>
</Col>
<Col>
<Row className={styles.name}>{follower.name}</Row>
<Row className={styles.account}>{follower.username}</Row>
<Col span={18}>
<Row className={styles.name}>
<Typography.Text ellipsis>{follower.name}</Typography.Text>
</Row>
<Row className={styles.account}>
<Typography.Text ellipsis>{follower.username}</Typography.Text>
</Row>
</Col>
</Row>
</a>

View File

@@ -4,9 +4,14 @@
display: grid;
width: 100%;
justify-items: center;
max-height: 75vh;
height: 75vh;
aspect-ratio: 16 / 9;
@media (max-width: 1200px) {
height: unset;
max-height: 75vh;
}
.player,
.poster {
width: 100%;

View File

@@ -1,23 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 46 42" style="enable-background:new 0 0 46 42;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_2_);}
</style>
<g>
<defs>
<path id="SVGID_1_" d="M22.2,24.2L8.5,39.9c-0.5,0.6-0.1,1.5,0.7,1.5h27.5c0.8,0,1.2-0.9,0.7-1.5L23.8,24.2
c-0.2-0.2-0.5-0.4-0.8-0.4C22.7,23.8,22.4,23.9,22.2,24.2 M6.5,0.6c-2.3,0-3.1,0.2-3.9,0.7C1.8,1.7,1.1,2.4,0.7,3.2
C0.2,4,0,4.9,0,7.1v17.5c0,2.3,0.2,3.1,0.7,3.9c0.4,0.8,1.1,1.5,1.9,1.9c0.8,0.4,1.7,0.7,3.9,0.7h5.2l2.2-2.6H5.8
c-1.1,0-1.6-0.1-2-0.3c-0.4-0.2-0.7-0.5-1-1c-0.2-0.4-0.3-0.8-0.3-2v-19c0-1.1,0.1-1.6,0.3-2c0.2-0.4,0.5-0.7,1-1
c0.4-0.2,0.8-0.3,2-0.3h34.3c1.1,0,1.6,0.1,2,0.3c0.4,0.2,0.7,0.5,1,1c0.2,0.4,0.3,0.8,0.3,2v19c0,1.1-0.1,1.6-0.3,2
c-0.2,0.4-0.5,0.7-1,1c-0.4,0.2-0.8,0.3-2,0.3H32l2.2,2.6h5.2c2.3,0,3.1-0.2,3.9-0.7c0.8-0.4,1.5-1.1,1.9-1.9
c0.4-0.8,0.7-1.7,0.7-3.9V7.1c0-2.3-0.2-3.1-0.7-3.9c-0.4-0.8-1.1-1.5-1.9-1.9c-0.8-0.4-1.7-0.7-3.9-0.7H6.5z"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<rect x="-6.4" y="-5.8" class="st0" width="58.7" height="53.6"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 46 42" style="enable-background:new 0 0 46 42" xml:space="preserve"><style type="text/css">.st0{clip-path:url(#SVGID_2_)}</style><g><defs><path id="SVGID_1_" d="M22.2,24.2L8.5,39.9c-0.5,0.6-0.1,1.5,0.7,1.5h27.5c0.8,0,1.2-0.9,0.7-1.5L23.8,24.2 c-0.2-0.2-0.5-0.4-0.8-0.4C22.7,23.8,22.4,23.9,22.2,24.2 M6.5,0.6c-2.3,0-3.1,0.2-3.9,0.7C1.8,1.7,1.1,2.4,0.7,3.2 C0.2,4,0,4.9,0,7.1v17.5c0,2.3,0.2,3.1,0.7,3.9c0.4,0.8,1.1,1.5,1.9,1.9c0.8,0.4,1.7,0.7,3.9,0.7h5.2l2.2-2.6H5.8 c-1.1,0-1.6-0.1-2-0.3c-0.4-0.2-0.7-0.5-1-1c-0.2-0.4-0.3-0.8-0.3-2v-19c0-1.1,0.1-1.6,0.3-2c0.2-0.4,0.5-0.7,1-1 c0.4-0.2,0.8-0.3,2-0.3h34.3c1.1,0,1.6,0.1,2,0.3c0.4,0.2,0.7,0.5,1,1c0.2,0.4,0.3,0.8,0.3,2v19c0,1.1-0.1,1.6-0.3,2 c-0.2,0.4-0.5,0.7-1,1c-0.4,0.2-0.8,0.3-2,0.3H32l2.2,2.6h5.2c2.3,0,3.1-0.2,3.9-0.7c0.8-0.4,1.5-1.1,1.9-1.9 c0.4-0.8,0.7-1.7,0.7-3.9V7.1c0-2.3-0.2-3.1-0.7-3.9c-0.4-0.8-1.1-1.5-1.9-1.9c-0.8-0.4-1.7-0.7-3.9-0.7H6.5z"/></defs><clipPath id="SVGID_2_"><use xlink:href="#SVGID_1_" style="overflow:visible"/></clipPath><rect width="58.7" height="53.6" x="-6.4" y="-5.8" class="st0"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -31,7 +31,7 @@
- This admin site chooses to have a generally Dark color palette, but with colors that are different from Ant design's _dark_ stylesheet, so that style sheet is not included. This results in a very large `ant-overrides.scss` file to reset colors on frequently used Ant components in the system. If you find yourself a new Ant Component that has not yet been used in this app, feel free to add a reset style for that component to the overrides stylesheet.
- Take a look at `variables.scss` CSS file if you want to give some elements custom css colors.
- Take a look at `variables.css` CSS file if you want to give some elements custom css colors.
---

View File

@@ -5,7 +5,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
module.exports = withBundleAnalyzer(
withLess({
productionBrowserSourceMaps: true,
productionBrowserSourceMaps: process.env.SOURCE_MAPS === 'true',
trailingSlash: true,
reactStrictMode: true,
images: {

1806
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@
},
"dependencies": {
"@ant-design/icons": "4.8.0",
"@codemirror/lang-css": "6.0.1",
"@codemirror/lang-css": "6.0.2",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/lang-markdown": "6.0.5",
"@codemirror/language-data": "6.1.0",
@@ -23,56 +23,56 @@
"@uiw/codemirror-theme-bbedit": "4.19.7",
"@uiw/react-codemirror": "4.19.7",
"@xstate/react": "3.0.2",
"antd": "4.24.3",
"antd": "4.24.7",
"autoprefixer": "10.4.13",
"chart.js": "4.2.0",
"chartkick": "5.0.1",
"chart.js": "^4.2.0",
"classnames": "2.3.2",
"date-fns": "2.29.3",
"entities": "^4.4.0",
"linkify-html": "^4.1.0",
"date-fns": "^2.29.3",
"interweave": "^13.0.0",
"interweave-autolink": "^5.1.0",
"linkifyjs": "^4.1.0",
"lodash": "4.17.21",
"next": "13.1.5",
"next": "13.1.6",
"next-with-less": "2.0.5",
"picmo": "5.7.2",
"picmo": "5.7.3",
"postcss-flexbugs-fixes": "5.0.2",
"prop-types": "15.8.1",
"react": "18.2.0",
"react-chartkick": "0.5.3",
"react-chartjs-2": "^5.2.0",
"react-chartkick": "^0.5.3",
"react-crossfade-img": "1.0.0",
"react-dom": "18.2.0",
"react-highlighter-ts": "18.0.1",
"react-hotkeys-hook": "4.3.2",
"react-hotkeys-hook": "4.3.4",
"react-linkify": "1.0.0-alpha",
"react-markdown": "8.0.5",
"react-use": "^17.4.0",
"react-virtuoso": "4.0.5",
"react-virtuoso": "4.0.7",
"recoil": "0.7.6",
"sharp": "0.31.3",
"slate": "0.88.1",
"slate-react": "0.88.2",
"ua-parser-js": "1.0.33",
"video.js": "7.20.3",
"xstate": "4.35.2",
"xstate": "4.35.4",
"yaml": "2.2.1"
},
"devDependencies": {
"@babel/core": "7.20.12",
"@mdx-js/react": "2.2.1",
"@storybook/addon-a11y": "6.5.15",
"@storybook/addon-actions": "6.5.15",
"@storybook/addon-docs": "6.5.15",
"@storybook/addon-essentials": "6.5.15",
"@storybook/addon-links": "6.5.15",
"@storybook/addon-a11y": "6.5.16",
"@storybook/addon-actions": "6.5.16",
"@storybook/addon-docs": "6.5.16",
"@storybook/addon-essentials": "6.5.16",
"@storybook/addon-links": "6.5.16",
"@storybook/addon-postcss": "2.0.0",
"@storybook/addon-viewport": "6.5.15",
"@storybook/builder-webpack5": "6.5.15",
"@storybook/cli": "6.5.15",
"@storybook/manager-webpack5": "6.5.15",
"@storybook/addon-viewport": "6.5.16",
"@storybook/builder-webpack5": "6.5.16",
"@storybook/cli": "6.5.16",
"@storybook/manager-webpack5": "6.5.16",
"@storybook/mdx2-csf": "0.0.3",
"@storybook/preset-scss": "1.0.3",
"@storybook/react": "6.5.15",
"@storybook/react": "6.5.16",
"@storybook/testing-library": "0.0.13",
"@svgr/webpack": "6.5.1",
"@types/chart.js": "2.9.37",
@@ -90,14 +90,14 @@
"chromatic": "6.15.0",
"css-loader": "6.7.3",
"cypress": "^12.0.0",
"eslint": "8.32.0",
"eslint": "8.33.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-next": "13.1.5",
"eslint-config-next": "13.1.6",
"eslint-config-prettier": "8.6.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-storybook": "0.6.10",
"handlebars": "^4.7.7",
@@ -105,16 +105,18 @@
"install": "^0.13.0",
"less": "4.1.3",
"less-loader": "11.1.0",
"mdx-mermaid": "^1.3.2",
"mermaid": "^9.3.0",
"npm": "^9.4.0",
"prettier": "2.8.3",
"sass": "1.57.1",
"sass": "1.58.0",
"sass-loader": "13.2.0",
"sb": "6.5.15",
"sb": "6.5.16",
"storybook-addon-designs": "6.3.1",
"storybook-addon-fetch-mock": "1.0.1",
"storybook-preset-less": "1.1.3",
"style-dictionary": "3.7.2",
"style-loader": "3.3.1",
"typescript": "4.9.4"
"typescript": "4.9.5"
}
}

View File

@@ -69,16 +69,19 @@ export default function HardwareInfo() {
name: 'CPU',
color: '#B63FFF',
data: hardwareStatus.cpu,
pointStyle: 'rect',
},
{
name: 'Memory',
color: '#2087E2',
data: hardwareStatus.memory,
pointStyle: 'circle',
},
{
name: 'Disk',
color: '#FF7700',
data: hardwareStatus.disk,
pointStyle: 'rectRounded',
},
];

View File

@@ -168,7 +168,7 @@ export default function Help() {
<div>
Most general questions are answered in our
<a
href="https://owncast.online/docs/faq/?source=admin"
href="https://owncast.online/faq/?source=admin"
target="_blank"
rel="noopener noreferrer"
>

View File

@@ -130,13 +130,13 @@ const StreamHealth = () => {
{
name: 'Errors',
color: '#B63FFF',
options: { radius: 3 },
data: errors,
pointStyle: 'crossRot',
pointRadius: 7,
},
{
name: 'Quality changes',
color: '#2087E2',
options: { radius: 2 },
data: qualityVariantChanges,
},
];
@@ -145,19 +145,16 @@ const StreamHealth = () => {
{
name: 'Median stream latency',
color: '#00FFFF',
options: { radius: 2 },
data: medianLatency,
},
{
name: 'Lowest stream latency',
color: '#02FD0D',
options: { radius: 2 },
data: lowestLatency,
},
{
name: 'Highest stream latency',
color: '#B63FFF',
options: { radius: 2 },
data: highestLatency,
},
];
@@ -188,6 +185,7 @@ const StreamHealth = () => {
time: item.time,
value: segmentLength,
})),
pointStyle: 'dash' as const,
options: { radius: 0 },
},
];
@@ -358,7 +356,7 @@ const StreamHealth = () => {
title="Seconds"
dataCollections={segmentDownloadDurationChart}
color="#FF7700"
unit="s"
unit="seconds"
yLogarithmic
/>
</Card>
@@ -420,7 +418,7 @@ const StreamHealth = () => {
title="Viewer Latency"
description="An approximate number of seconds that your viewers are behind your live video. The largest cause of latency spikes is buffering. High latency itself is not a problem, and optimizing for low latency can result in buffering, resulting in even higher latency."
/>
<Chart title="Seconds" dataCollections={latencyChart} color="#FF7700" unit="s" />
<Chart title="Seconds" dataCollections={latencyChart} color="#FF7700" unit="seconds" />
</Card>
</Space>
</>

View File

@@ -151,7 +151,7 @@ export default function ViewersOverTime() {
</button>
</Dropdown>
{viewerInfo.length > 0 && (
<Chart title="Viewers" data={viewerInfo} color="#2087E2" unit="" />
<Chart title="Viewers" data={viewerInfo} color="#2087E2" unit="viewers" />
)}
<ViewerTable data={viewerDetails} />

View File

@@ -1,21 +1,26 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import { useRouter } from 'next/router';
import { Skeleton } from 'antd';
import {
clientConfigStateAtom,
ClientConfigStore,
isOnlineSelector,
serverStatusState,
appStateAtom,
} from '../../../components/stores/ClientConfigStore';
import { OfflineBanner } from '../../../components/ui/OfflineBanner/OfflineBanner';
import { Statusbar } from '../../../components/ui/Statusbar/Statusbar';
import { OwncastPlayer } from '../../../components/video/OwncastPlayer/OwncastPlayer';
import { ClientConfig } from '../../../interfaces/client-config.model';
import { ServerStatus } from '../../../interfaces/server-status.model';
import { AppStateOptions } from '../../../components/stores/application-state';
import { Theme } from '../../../components/theme/Theme';
export default function VideoEmbed() {
const status = useRecoilValue<ServerStatus>(serverStatusState);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
const { name } = clientConfig;
@@ -39,34 +44,45 @@ export default function VideoEmbed() {
const initiallyMuted = query.initiallyMuted === 'true';
const loadingState = <Skeleton active style={{ padding: '10px' }} paragraph={{ rows: 10 }} />;
const offlineState = (
<OfflineBanner streamName={name} customText={offlineMessage} notificationsEnabled={false} />
);
const onlineState = (
<>
<OwncastPlayer
source="/hls/stream.m3u8"
online={online}
initiallyMuted={initiallyMuted}
title={streamTitle || name}
/>
<Statusbar
online={online}
lastConnectTime={lastConnectTime}
lastDisconnectTime={lastDisconnectTime}
viewerCount={viewerCount}
/>
</>
);
const getView = () => {
if (appState.appLoading) {
return loadingState;
}
if (online) {
return onlineState;
}
return offlineState;
};
return (
<>
<ClientConfigStore />
<div className="video-embed">
{online && (
<OwncastPlayer
source="/hls/stream.m3u8"
online={online}
initiallyMuted={initiallyMuted}
title={streamTitle || name}
/>
)}
{!online && (
<OfflineBanner
streamName={name}
customText={offlineMessage}
notificationsEnabled={false}
/>
)}
{online && (
<Statusbar
online={online}
lastConnectTime={lastConnectTime}
lastDisconnectTime={lastDisconnectTime}
viewerCount={viewerCount}
/>
)}
</div>
<Theme />
<div className="video-embed">{getView()}</div>
</>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,14 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 520 520" style="enable-background:new 0 0 520 520;" xml:space="preserve">
<path d="M13.7,11.9v496.2h35.7V520H0V0h49.4v11.9H13.7z"/>
<path d="M166.3,169.2v25.1h0.7c6.7-9.6,14.8-17,24.2-22.2c9.4-5.3,20.3-7.9,32.5-7.9c11.7,0,22.4,2.3,32.1,6.8
c9.7,4.5,17,12.6,22.1,24c5.5-8.1,13-15.3,22.4-21.5c9.4-6.2,20.6-9.3,33.5-9.3c9.8,0,18.9,1.2,27.3,3.6c8.4,2.4,15.5,6.2,21.5,11.5
c6,5.3,10.6,12.1,14,20.6c3.3,8.5,5,18.7,5,30.7v124.1h-50.9V249.6c0-6.2-0.2-12.1-0.7-17.6c-0.5-5.5-1.8-10.3-3.9-14.3
c-2.2-4.1-5.3-7.3-9.5-9.7c-4.2-2.4-9.9-3.6-17-3.6c-7.2,0-13,1.4-17.4,4.1c-4.4,2.8-7.9,6.3-10.4,10.8c-2.5,4.4-4.2,9.4-5,15.1
c-0.8,5.6-1.3,11.3-1.3,17v103.3h-50.9v-104c0-5.5-0.1-10.9-0.4-16.3c-0.2-5.4-1.3-10.3-3.1-14.9c-1.8-4.5-4.8-8.2-9-10.9
c-4.2-2.7-10.3-4.1-18.5-4.1c-2.4,0-5.6,0.5-9.5,1.6c-3.9,1.1-7.8,3.1-11.5,6.1c-3.7,3-6.9,7.3-9.5,12.9c-2.6,5.6-3.9,13-3.9,22.1
v107.6h-50.9V169.2H166.3z"/>
<path d="M506.3,508.1V11.9h-35.7V0H520v520h-49.4v-11.9H506.3z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 520 520" style="enable-background:new 0 0 520 520" xml:space="preserve"><path d="M13.7,11.9v496.2h35.7V520H0V0h49.4v11.9H13.7z"/><path d="M166.3,169.2v25.1h0.7c6.7-9.6,14.8-17,24.2-22.2c9.4-5.3,20.3-7.9,32.5-7.9c11.7,0,22.4,2.3,32.1,6.8 c9.7,4.5,17,12.6,22.1,24c5.5-8.1,13-15.3,22.4-21.5c9.4-6.2,20.6-9.3,33.5-9.3c9.8,0,18.9,1.2,27.3,3.6c8.4,2.4,15.5,6.2,21.5,11.5 c6,5.3,10.6,12.1,14,20.6c3.3,8.5,5,18.7,5,30.7v124.1h-50.9V249.6c0-6.2-0.2-12.1-0.7-17.6c-0.5-5.5-1.8-10.3-3.9-14.3 c-2.2-4.1-5.3-7.3-9.5-9.7c-4.2-2.4-9.9-3.6-17-3.6c-7.2,0-13,1.4-17.4,4.1c-4.4,2.8-7.9,6.3-10.4,10.8c-2.5,4.4-4.2,9.4-5,15.1 c-0.8,5.6-1.3,11.3-1.3,17v103.3h-50.9v-104c0-5.5-0.1-10.9-0.4-16.3c-0.2-5.4-1.3-10.3-3.1-14.9c-1.8-4.5-4.8-8.2-9-10.9 c-4.2-2.7-10.3-4.1-18.5-4.1c-2.4,0-5.6,0.5-9.5,1.6c-3.9,1.1-7.8,3.1-11.5,6.1c-3.7,3-6.9,7.3-9.5,12.9c-2.6,5.6-3.9,13-3.9,22.1 v107.6h-50.9V169.2H166.3z"/><path d="M506.3,508.1V11.9h-35.7V0H520v520h-49.4v-11.9H506.3z"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,120 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<svg:svg
version="1.1"
xml:space="preserve"
viewBox="0 0 200 200"
width="200px"
height="200px"
x="0px"
y="0px"
enable-background="new 0 0 200 200"
class="TridactylThemeDark"
id="svg36"
sodipodi:docname="xmpp.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:xhtml="http://www.w3.org/1999/xhtml"><svg:defs
id="defs40" /><sodipodi:namedview
id="namedview38"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="3.337544"
inkscape:cx="134.53006"
inkscape:cy="118.05088"
inkscape:window-width="3834"
inkscape:window-height="1568"
inkscape:window-x="0"
inkscape:window-y="26"
inkscape:window-maximized="1"
inkscape:current-layer="svg36" />
<svg:linearGradient
id="SVGID_right_"
y2="1.279e-13"
gradientUnits="userSpaceOnUse"
x2="-1073.2"
gradientTransform="matrix(1.1429608,0,0,1.1429608,1352.3375,27.297)"
y1="126.85"
x1="-1073.2">
<svg:stop
stop-color="#1b3967"
offset=".011"
id="stop2" />
<svg:stop
stop-color="#13b5ea"
offset=".467"
id="stop4" />
<svg:stop
stop-color="#002b5c"
offset=".9945"
id="stop6" />
</svg:linearGradient>
<svg:linearGradient
id="SVGID_left_"
y2="1.279e-13"
gradientUnits="userSpaceOnUse"
x2="-1073.2"
gradientTransform="matrix(-1.1429608,0,0,1.1429608,-1152.3376,27.295856)"
y1="126.85"
x1="-1073.2">
<svg:stop
stop-color="#1b3967"
offset=".011"
id="stop9" />
<svg:stop
stop-color="#13b5ea"
offset=".467"
id="stop11" />
<svg:stop
stop-color="#002b5c"
offset=".9945"
id="stop13" />
</svg:linearGradient>
<svg:path
d="m 158.17335,43.514472 c 0.088,1.500707 -2.04247,1.106386 -2.04247,2.620809 0,44.062284 -53.21398,111.285539 -104.795782,124.274139 v 1.87332 C 119.85902,165.9736 198.27413,94.789982 200,27.298143 l -41.83123,16.217472 z"
style="fill:url(#SVGID_right_);stroke-width:1.14296"
id="path16" />
<svg:path
d="m 137.44918,48.935535 c 0.0868,1.500708 0.13715,3.005988 0.13715,4.522696 0,44.062284 -35.08773,103.434549 -86.667269,116.421999 v 1.87332 C 118.40404,168.56469 171.85574,99.719572 171.85574,46.942212 c 0,-2.714532 -0.1463,-5.405062 -0.42403,-8.064732 l -33.98023,10.05577 z"
style="fill:#e96d1f;stroke-width:1.14296"
id="path18" />
<svg:path
d="m 171.75858,38.249995 -8.70592,3.111139 c 0.0469,1.099528 0.0755,2.576234 0.0755,3.686048 0,47.111704 -42.59929,112.243348 -99.748468,122.433978 -3.708919,1.24354 -8.61565,2.37393 -12.494848,3.35002 v 1.87217 C 125.46753,166.34848 177.86772,90.563313 171.7643,38.245422 Z"
style="fill:#d9541e;stroke-width:1.14296"
id="path20" />
<svg:path
d="m 41.826653,43.513329 c -0.088,1.500708 2.042471,1.106387 2.042471,2.620809 0,44.062284 53.21398,111.285542 104.795786,124.274152 v 1.87331 C 80.140986,165.97245 1.7258709,94.78884 0,27.297 l 41.831225,16.217472 z"
style="fill:url(#SVGID_left_);stroke-width:1.14296"
id="path22" />
<svg:path
d="m 62.550819,48.934392 c -0.08687,1.500708 -0.137155,3.005988 -0.137155,4.522697 0,44.062284 35.087749,103.434541 86.667286,116.422011 v 1.87331 C 81.595976,168.56354 28.144269,99.718429 28.144269,46.941069 c 0,-2.714533 0.146298,-5.405062 0.424038,-8.064732 l 33.980226,10.05577 z"
style="fill:#a0ce67;stroke-width:1.14296"
id="path24" />
<svg:path
d="m 28.241419,38.248851 8.705933,3.111139 c -0.04686,1.099529 -0.07543,2.576234 -0.07543,3.68605 0,47.111704 42.599295,112.24334 99.748468,122.43398 3.7089,1.24353 8.61563,2.37393 12.49485,3.35002 v 1.87216 C 74.532478,166.34735 22.132294,90.56217 28.235705,38.24428 Z"
style="fill:#439639;stroke-width:1.14296"
id="path26" />
<script /><xhtml:iframe
class="cleanslate hidden"
src="xmpp_files/commandline.html"
id="cmdline_iframe"
loading="lazy"
style="height: 0px !important;" /></svg:svg>
<svg:svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xhtml="http://www.w3.org/1999/xhtml" id="svg36" width="200" height="200" x="0" y="0" class="TridactylThemeDark" enable-background="new 0 0 200 200" version="1.1" viewBox="0 0 200 200" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)" sodipodi:docname="xmpp.svg" xml:space="preserve"><svg:defs id="defs40"/><sodipodi:namedview id="namedview38" bordercolor="#ffffff" borderopacity="1" pagecolor="#505050" showgrid="false" inkscape:current-layer="svg36" inkscape:cx="134.53" inkscape:cy="118.051" inkscape:deskcolor="#505050" inkscape:pagecheckerboard="1" inkscape:pageopacity="0" inkscape:showpageshadow="0" inkscape:window-height="1568" inkscape:window-maximized="1" inkscape:window-width="3834" inkscape:window-x="0" inkscape:window-y="26" inkscape:zoom="3.338"/><svg:linearGradient id="SVGID_right_" x1="-1073.2" x2="-1073.2" y1="126.85" y2="0" gradientTransform="matrix(1.1429608,0,0,1.1429608,1352.3375,27.297)" gradientUnits="userSpaceOnUse"><svg:stop id="stop2" offset=".011" stop-color="#1b3967"/><svg:stop id="stop4" offset=".467" stop-color="#13b5ea"/><svg:stop id="stop6" offset=".995" stop-color="#002b5c"/></svg:linearGradient><svg:linearGradient id="SVGID_left_" x1="-1073.2" x2="-1073.2" y1="126.85" y2="0" gradientTransform="matrix(-1.1429608,0,0,1.1429608,-1152.3376,27.295856)" gradientUnits="userSpaceOnUse"><svg:stop id="stop9" offset=".011" stop-color="#1b3967"/><svg:stop id="stop11" offset=".467" stop-color="#13b5ea"/><svg:stop id="stop13" offset=".995" stop-color="#002b5c"/></svg:linearGradient><svg:path d="m 158.17335,43.514472 c 0.088,1.500707 -2.04247,1.106386 -2.04247,2.620809 0,44.062284 -53.21398,111.285539 -104.795782,124.274139 v 1.87332 C 119.85902,165.9736 198.27413,94.789982 200,27.298143 l -41.83123,16.217472 z" style="fill:url(#SVGID_right_);stroke-width:1.14296" id="path16"/><svg:path d="m 137.44918,48.935535 c 0.0868,1.500708 0.13715,3.005988 0.13715,4.522696 0,44.062284 -35.08773,103.434549 -86.667269,116.421999 v 1.87332 C 118.40404,168.56469 171.85574,99.719572 171.85574,46.942212 c 0,-2.714532 -0.1463,-5.405062 -0.42403,-8.064732 l -33.98023,10.05577 z" style="fill:#e96d1f;stroke-width:1.14296" id="path18"/><svg:path d="m 171.75858,38.249995 -8.70592,3.111139 c 0.0469,1.099528 0.0755,2.576234 0.0755,3.686048 0,47.111704 -42.59929,112.243348 -99.748468,122.433978 -3.708919,1.24354 -8.61565,2.37393 -12.494848,3.35002 v 1.87217 C 125.46753,166.34848 177.86772,90.563313 171.7643,38.245422 Z" style="fill:#d9541e;stroke-width:1.14296" id="path20"/><svg:path d="m 41.826653,43.513329 c -0.088,1.500708 2.042471,1.106387 2.042471,2.620809 0,44.062284 53.21398,111.285542 104.795786,124.274152 v 1.87331 C 80.140986,165.97245 1.7258709,94.78884 0,27.297 l 41.831225,16.217472 z" style="fill:url(#SVGID_left_);stroke-width:1.14296" id="path22"/><svg:path d="m 62.550819,48.934392 c -0.08687,1.500708 -0.137155,3.005988 -0.137155,4.522697 0,44.062284 35.087749,103.434541 86.667286,116.422011 v 1.87331 C 81.595976,168.56354 28.144269,99.718429 28.144269,46.941069 c 0,-2.714533 0.146298,-5.405062 0.424038,-8.064732 l 33.980226,10.05577 z" style="fill:#a0ce67;stroke-width:1.14296" id="path24"/><svg:path d="m 28.241419,38.248851 8.705933,3.111139 c -0.04686,1.099529 -0.07543,2.576234 -0.07543,3.68605 0,47.111704 42.599295,112.24334 99.748468,122.43398 3.7089,1.24353 8.61563,2.37393 12.49485,3.35002 v 1.87216 C 74.532478,166.34735 22.132294,90.56217 28.235705,38.24428 Z" style="fill:#439639;stroke-width:1.14296" id="path26"/><script/><xhtml:iframe id="cmdline_iframe" class="cleanslate hidden" loading="lazy" src="xmpp_files/commandline.html" style="height:0!important"/></svg:svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -21,19 +21,17 @@
.chat-messages .bulk-editor {
margin: 0.5rem 0;
padding: 0.5rem;
border: 1px solid var(--textfield-border);
border: 1px solid var(--theme-color-palette-8);
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
border-radius: 4px;
}
.chat-messages .bulk-editor.active .label {
color: var(--black-3);
}
.chat-messages .bulk-editor .label {
font-size: 0.75rem;
color: var(--white-50);
color: var(--theme-color-palette-10);
margin-right: 0.5rem;
}
.chat-messages .bulk-editor button {
@@ -67,7 +65,7 @@
opacity: 1;
}
.toggle-switch .ant-btn-text:hover {
background-color: var(--black-35);
background-color: var(--theme-color-palette-10);
}
.blockuser-popover {
max-width: 400px;
@@ -82,14 +80,14 @@
outline: none;
}
.user-item-container .display-name {
color: var(--white);
border-bottom: 1px dotted var(--white-50);
color: var(--theme-color-palette-4);
border-bottom: 1px dotted var(--theme-color-palette-10);
}
.user-item-container:hover .display-name {
border-color: var(--white);
border-color: var(--theme-color-palette-4);
}
.user-details h5 {
color: var(--white);
color: var(--theme-color-palette-4);
}
.user-details .created-at {
font-size: 0.75em;
@@ -110,11 +108,9 @@
.user-details .previous-names-list .latest .user-name-item {
font-weight: bold;
font-style: normal;
color: var(--pink);
}
.user-details .ant-divider {
border-color: var(--white-25);
color: var(--theme-color-palette-12);
}
.block-user-button {
text-transform: capitalize;
}

View File

@@ -12,13 +12,12 @@
margin-left: 0.3rem;
padding: 2px;
border-radius: 5rem;
color: var(--black);
border: 1px solid var(--black);
border: 1px solid black;
transition-duration: var(--ant-transition-duration);
}
.edit-current-strings .ant-tag .ant-tag-close-icon:hover {
border-color: var(--owncast-purple);
background-color: var(--white);
border-color: var(--theme-color-palette-6);
background-color: var(--theme-color-palette-4);
}
.edit-current-strings .ant-tag .ant-tag-close-icon:hover svg {
fill: black;

View File

@@ -41,13 +41,13 @@
width: 100%;
}
.config-public-details-page .social-items-container {
background-color: var(--container-bg-color-alt);
background-color: var(--theme-color-palette-5);
padding: 0 0.75em;
margin-left: 1em;
max-width: 450px;
}
.config-public-details-page .social-items-container .form-module {
background-color: var(--black);
background-color: var(--theme-color-palette-10);
}
.config-public-details-page .social-items-container .social-handles-container {
min-width: 350px;
@@ -78,5 +78,5 @@
max-width: 256px;
margin-right: 1em;
display: inline-block;
border: 1px solid var(--white-25);
border: 1px solid var(--theme-color-palette-5);
}

View File

@@ -31,7 +31,6 @@
flex-direction: row;
align-items: center;
justify-content: flex-start;
color: var(--white-75);
}
.social-links-edit-container .social-handles-table .social-handle-cell .option-icon {
height: 2em;

View File

@@ -10,11 +10,11 @@
.config-variant-form .passthrough-warning {
text-align: center;
padding: 1em;
color: var(--ant-warning);
color: var(--theme-color-palette-error);
font-size: 0.88em;
font-weight: 500;
background-color: var(--black-50);
border-radius: var(--container-border-radius);
background-color: var(--theme-color-palette-10);
border-radius: var(--theme-rounded-corners);
}
.config-variant-form .cpu-usage-container,
.config-variant-form .bitrate-container {
@@ -43,9 +43,7 @@
.read-more-subtext {
font-size: 0.8rem;
}
.codec-module .ant-collapse-content-active {
background-color: var(--white-15);
}
.video-text-field-container {
margin-left: 12px;
margin-bottom: 30px;

View File

@@ -23,7 +23,7 @@
}
.field-tip {
font-size: 0.8em;
color: var(--white-50);
color: var(--theme-color-palette-10);
}
.field-container {
padding: 0.85em 0 0.5em;
@@ -32,8 +32,8 @@
width: 100%;
margin: auto;
padding: 1em 2em 0.75em;
background-color: var(--owncast-purple-25);
border-radius: var(--container-border-radius);
background-color: var(--color-owncast-palette-7);
border-radius: var(--theme-rounded-corners);
}
.segment-slider-container .status-container {
width: 100%;
@@ -46,10 +46,10 @@
text-align: center;
font-size: 0.75em;
line-height: normal;
color: var(--white);
color: var(--theme-color-palette-4);
padding: 1em;
border-radius: var(--container-border-radius);
background-color: var(--black-35);
border-radius: var(--theme-rounded-corners);
background-color: var(--theme-color-palette-error);
}
.segment-tip {
width: 10em;

View File

@@ -2,7 +2,7 @@
margin-bottom: 1em;
}
.home-container .online-status-section .online-details-card {
border-color: var(--online-color);
border-color: var(--theme-color-palette-6);
}
.home-container .stream-details-item-container {
margin: 1em 0;
@@ -17,8 +17,8 @@
margin-bottom: 1em;
}
.offline-content .list-section {
background-color: var(--container-bg-color-alt);
border-radius: var(--container-border-radius);
/* background-color: var(--theme-color-palette-3); */
border-radius: var(--theme-rounded-corners);
padding: 1em;
}
.offline-content .list-section > .ant-card {
@@ -38,17 +38,18 @@
.news-feed {
margin-top: 0;
padding: 1.5em;
border: 1px solid var(--theme-color-palette-15);
}
.news-feed h2 {
font-size: 1.2em;
margin-top: 0;
color: var(--pink);
color: var(--theme-color-palette-11);
}
.news-feed article {
padding: 1em 0.25em;
font-size: 0.9rem;
color: var(--white-75);
border-bottom: 1px solid var(--gray);
border-bottom: 1px solid var(--theme-color-palette-10);
}
.news-feed article h3 {
font-size: 1.2em;
@@ -60,5 +61,14 @@
.news-feed article .timestamp {
margin-top: 0;
font-size: 0.75em;
color: var(--white-50);
}
.ant-collapse > .ant-collapse-item:last-child,
.ant-collapse > .ant-collapse-item:last-child > .ant-collapse-header {
background-color: var(--theme-color-palette-5);
}
.ant-card-body {
/* background-color: var(--theme-color-palette-5); */
border: 1px solid var(--theme-color-palette-15);
}

View File

@@ -5,11 +5,12 @@
height: 100vh;
overflow: auto;
z-index: 10;
background-color: var(--theme-color-background-main);
background-color: var(--theme-color-palette-1);
}
.app-container .menu-container {
border-color: transparent;
background-color: unset;
background-color: unset;
color: var(--theme-color-components-text-on-dark);
}
.app-container h1.owncast-title {
padding: 1rem;
@@ -20,7 +21,7 @@
align-items: center;
}
.app-container h1.owncast-title .logo-container {
background-color: var(--white);
background-color: var(--theme-color-palette-4);
padding-top: 4px;
padding-right: 6px;
padding-left: 6px;
@@ -29,7 +30,7 @@
.app-container h1.owncast-title .title-label {
display: inline-block;
margin-left: 1rem;
color: var(--white);
color: var(--theme-color-palette-4);
font-size: 1.15rem;
font-weight: 200;
text-transform: uppercase;
@@ -45,9 +46,10 @@
justify-content: flex-end;
padding-right: 1rem;
padding-left: 1rem;
background-color: var(--nav-bg-color);
background-color: var(--theme-color-background-main);
}
.app-container .main-content-container {
background-color: var(--theme-color-palette-3);
padding: 2em 3em 3em;
min-width: 50vw;
}
@@ -64,7 +66,7 @@
width: 12.5rem;
}
.app-container .online-status-indicator .status-label {
color: var(--white);
color: var(--theme-color-palette-4);
text-transform: uppercase;
font-size: 0.75rem;
display: inline-block;
@@ -135,3 +137,9 @@
.ant-select:not(.ant-select-customize-input) .ant-select-selector {
background-color: var(--theme-color-components-form-field-background);
}
.ant-menu-item a,
.ant-menu-submenu-expand-icon,
.ant-menu-submenu-arrow {
color: unset;
}

View File

@@ -14,7 +14,7 @@ export default {
title: 'owncast/Chat/Embeds/Read-write chat',
component: ReadWritePage,
parameters: {
chromatic: { diffThreshold: 0.2 },
chromatic: { diffThreshold: 0.8 },
},
} as ComponentMeta<typeof ReadWritePage>;

View File

@@ -1,69 +0,0 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
const Template = ({
origin,
query,
title,
width,
height,
}: {
origin: string;
query: string;
title: string;
width: number;
height: number;
}) => (
<iframe
src={`${origin}/embed/video?${query}`}
title={title}
height={`${height}px`}
width={`${width}px`}
referrerPolicy="origin"
scrolling="no"
allowFullScreen
/>
);
const origins = {
DemoServer: `https://watch.owncast.online`,
RetroStrangeTV: `https://live.retrostrange.com`,
localhost: `http://localhost:3000`,
};
export default {
title: 'owncast/Player/Embeds',
component: Template,
argTypes: {
origin: {
options: Object.keys(origins),
mapping: origins,
control: {
type: 'select',
},
defaultValue: origins.DemoServer,
},
query: {
type: 'string',
},
title: {
defaultValue: 'My Title',
type: 'string',
},
height: {
defaultValue: 350,
type: 'number',
},
width: {
defaultValue: 550,
type: 'number',
},
},
} satisfies ComponentMeta<typeof Template>;
export const Default: ComponentStory<typeof Template> = Template.bind({});
Default.args = {};
export const InitiallyMuted: ComponentStory<typeof Template> = Template.bind({});
InitiallyMuted.args = {
query: 'initiallyMuted=true',
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

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