Merge branch 'webv2' into fix/ImplementPasswordRules

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

View File

@@ -18,7 +18,6 @@ module.exports = {
'@storybook/addon-postcss',
'@storybook/addon-a11y',
'storybook-addon-designs',
'storybook-dark-mode',
'storybook-addon-fetch-mock',
],
webpackFinal: async (config, { configType }) => {
@@ -45,7 +44,9 @@ module.exports = {
},
],
});
return config;
},
framework: '@storybook/react',
staticDirs: ['../public', '../../static', './story-assets'],
};

View File

@@ -1,13 +1,16 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { ColorRow } from './Color';
<Meta title="owncast/Style Guide/Default Theme" />
<Meta title="owncast/Styles/Colors" />
# Default theme colors
These colors are assigned in our [color token](https://github.com/owncast/owncast/tree/webv2/web/style-definitions/tokens/color) files
and get reflected here as they change. run `npm run build-styles` to regenerate.
<Story
name="Default Theme"
>
## Default Theme
These color names are assigned to specific component variables. They can be overwritten via CSS.
@@ -39,7 +42,11 @@ These color names are assigned to specific component variables. They can be over
'theme-color-action-disabled',
]}
/>
</Story>
<Story
name="Frontend Components"
>
## Component Colors
<ColorRow
@@ -72,9 +79,15 @@ These color names are assigned to specific component variables. They can be over
'theme-color-components-form-field-placeholder',
'theme-color-components-form-field-text',
'theme-color-components-form-field-border',
'theme-color-components-video-status-bar-background',
'theme-color-components-video-status-bar-foreground',
]}
/>
</Story>
<Story
name="Owncast Color Palette"
>
## Default Palette
These are the core colors for the default, out of the box, Owncast web application theme.
@@ -98,7 +111,11 @@ They should not be overwritten, instead the theme variables should be overwritte
'color-owncast-palette-15',
]}
/>
</Story>
<Story
name="User Chat Colors"
>
## User Colors
<ColorRow
@@ -113,3 +130,4 @@ They should not be overwritten, instead the theme variables should be overwritte
'theme-color-users-7',
]}
/>
</Story>

View File

@@ -0,0 +1,64 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/Design" />
# Owncast Design Guidelines & Resources
A collection of design contribution guidelines and resources for the Owncast interface.
> **All participating designers are highly encouraged to shape and evolve these guidelines!**
> It is a work in progress and as we have design contributors we can work to solidify the process, tools and resources.
## 👋 Welcome
Owncast is a is a live streaming and chat server targeted to anybody who has live streaming needs. This means anybody from corporate events, government meetings, game streamers, musicians, churches, TV stations, and more.
Read the detailed [product definition](https://github.com/owncast/owncast/blob/develop/docs/product-definition.md) to learn more.
## 🚢 How to contribute to product design
1. Check out open [issues](https://github.com/owncast/owncast/issues) here on GitHub (we label them with `needs design`)
2. Feel free to open an issue on your own if you find something you would like to contribute to the project.
3. Add your contributions to an issue and we promise we will review your contribution carefully and foster discussions
**We encourage you to:**
- Get in touch with the team by joining our [Community Chat](https://owncast.rocket.chat).
- Check out our [Contributor Guide](https://owncast.online/help) and
[Code of Conduct](https://github.com/owncast/owncast/blob/develop/CODE_OF_CONDUCT.md)
## 🎭 Target audience
Owncast is a is a live streaming and chat server targeted to anybody who has live streaming needs. This means anything from corporate events, government meetings, game streams, concerts, TV stations, and more.
## 💅 Design relevant materials
Here is a list of design relevant information and materials:
### Fonts
http://owncast.online/components/?path=/story/owncast-style-guide-typography--page
Body text: OpenSans
Display/Header text: Poppins
### Colors
http://owncast.online/components/?path=/story/owncast-style-guide-default-theme--page
### Design Files, Screenshots, etc
We do not currently have any design files that fully represent the state of
the Owncast interface. However going forward it would be nice to resolve this
and collaborate on designs.
We do have a [PenPot organization](https://design.penpot.app/#/dashboard/team/8373f780-f255-11ec-b774-f940e3befd53/projects). Please ask for access.
## 🎓 License
All design work is licensed under the
[MIT](https://mit-license.org/)
[(Back to top)](#-table-of-contents)

View File

@@ -0,0 +1,85 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/Get Started with Owncast Development" />
---
title: "How to work on Owncast"
description: The technical details for those wishing to take part in Owncast development.
tags:
[
development,
contribute,
open-source,
github,
git,
go,
react,
typescript,
contributing,
]
aliases: [/docs/building]
type: toc
toc: true
---
Owncast is a straightforward web application and compared to many projects is very easy to get running locally and contributing to.
- The backend is written in [Go](https://go.dev/).
- The frontend is written in [React](https://reactjs.org/).
If you're interested in contributing to Owncast, here's how you can get started.
## How to start with Frontend development
The web frontend of Owncast is written in React with TypeScript built using [Next.js](https://nextjs.org/).
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`.
### Run the web project
1. Change to the `web` directory and install dependencies with `npm install`.
1. Start the development server with `npm run dev`.
1. Open `http://localhost:3000` in your browser.
You must have an instance of Owncast running locally to connect to. You can run one with `go run main.go` from the root of the repository. Read more details about running development Owncast under the backend section.
### 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.
### Use Storybook to update and create components
Storybook is a tool that allows you to create and test components in isolation. It's a great way to develop new components and test them out without running a copy of the Owncast server.
1. Run `npm run storybook` to start the Storybook server.
1. Open `http://localhost:6006` in your browser.
1. Navigate the Storybook interface to browse and test components.
## How to start with Backend development
The backend of Owncast is written in Go. It operates as a web and API server, inbound RTMP ingestion server, outbound HLS distribution server, and chat server.
1. Ensure you have the [Go programming language](https://go.dev/dl/) tools installed for your system.
1. Clone the Owncast repository with `git clone https://github.com/owncast/owncast`.
1. A c compiler and tooling must be available on your system. Generally this means installing `gcc` and its development libraries.
1. Run `go run main.go` from the root of the repository.
### Go Linting
We use golangci-lint to lint our Go code. While optional, it is a useful tool to assist you in writing better Go code. You can install it from the [golangci-lint](https://golangci-lint.run/usage/install/#local-installation) website.
## Run a development stream
Many features are only enabled when a stream is live. You can run a local stream using any video file you have around by running:
```bash
./test/ocTestStream.sh somevideofile.mp4
```
## If you haven't yet, find an issue to work on
Visit our [Good First Issues](https://github.com/owncast/owncast/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) list to find something that might be a good fit for you to start on. Otherwise, feel free to drop into our [community chat](https://owncast.rocket.chat) and say hi and we can get to know you and see where you'd like to take part.

View File

@@ -0,0 +1,148 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Frontend Assets/Emoji" />
# Built-in Custom Emoji
## Blob
<Story
name="Blob"
>
<a href="img/emoji/blob/LICENSE.md" target="_blank">
LICENSE
</a>
<ImageRow images={[
{src: "img/emoji/blob/ablobattention.gif", name: "ablobattention.gif"},
{src: "img/emoji/blob/ablobaww.gif", name: "ablobaww.gif"},
{src: "img/emoji/blob/ablobblewobble.gif", name: "ablobblewobble.gif"},
{src: "img/emoji/blob/ablobcheer.gif", name: "ablobcheer.gif"},
{src: "img/emoji/blob/ablobcry.gif", name: "ablobcry.gif"},
{src: "img/emoji/blob/ablobdancer.gif", name: "ablobdancer.gif"},
{src: "img/emoji/blob/ablobgift.gif", name: "ablobgift.gif"},
{src: "img/emoji/blob/ablobgiggle.gif", name: "ablobgiggle.gif"},
{src: "img/emoji/blob/ablobparty.gif", name: "ablobparty.gif"},
{src: "img/emoji/blob/ablobsleep.gif", name: "ablobsleep.gif"},
{src: "img/emoji/blob/ablobthinking.gif", name: "ablobthinking.gif"},
{src: "img/emoji/blob/ablobwave.gif", name: "ablobwave.gif"},
{src: "img/emoji/blob/blobangry.png", name: "blobangry.png"},
{src: "img/emoji/blob/blobaww.png", name: "blobaww.png"},
{src: "img/emoji/blob/blobdancer.png", name: "blobdancer.png"},
{src: "img/emoji/blob/blobjam.png", name: "blobjam.png"},
{src: "img/emoji/blob/blobscream.png", name: "blobscream.png"},
{src: "img/emoji/blob/blobthanks.png", name: "blobthanks.png"},
{src: "img/emoji/blob/blobthinking.png", name: "blobthinking.png"},
{src: "img/emoji/blob/blobwave.png", name: "blobwave.png"},
{src: "img/emoji/blob/blobyes.png", name: "blobyes.png"},
{src: "img/emoji/blob/blobyum.png", name: "blobyum.png"},
]}/>
</Story>
## Conigliolo96
<Story
name="Conigliolo96"
>
<a href="img/emoji/conigliolo96/LICENSE.md" target="_blank">
LICENSE
</a>
<ImageRow images={[
{src: "img/emoji/conigliolo96/conigliolo1.gif", name: "conigliolo1.gif"},
{src: "img/emoji/conigliolo96/conigliolo15.gif", name: "conigliolo15.gif"},
{src: "img/emoji/conigliolo96/conigliolo17.gif", name: "conigliolo17.gif"},
{src: "img/emoji/conigliolo96/conigliolo21.gif", name: "conigliolo21.gif"},
{src: "img/emoji/conigliolo96/conigliolo25.gif", name: "conigliolo25.gif"},
{src: "img/emoji/conigliolo96/conigliolo28.gif", name: "conigliolo28.gif"},
]}/>
</Story>
## Dog
<Story
name="Dog"
>
<a href="img/emoji/dog/LICENSE.md" target="_blank">
LICENSE
</a>
<ImageRow images={[
{src: "img/emoji/dog/img001.svg", name: "img001.svg"},
{src: "img/emoji/dog/img091.svg", name: "img091.svg"},
{src: "img/emoji/dog/img093.svg", name: "img093.svg"},
{src: "img/emoji/dog/img203.svg", name: "img203.svg"},
{src: "img/emoji/dog/img288.svg", name: "img288.svg"},
{src: "img/emoji/dog/img327.svg", name: "img327.svg"},
{src: "img/emoji/dog/img346.svg", name: "img346.svg"},
{src: "img/emoji/dog/img347.svg", name: "img347.svg"},
{src: "img/emoji/dog/img352.svg", name: "img352.svg"},
]}/>
</Story>
## Mutant
<Story
name="Mutant"
>
<a href="img/emoji/mutant/LICENSE.md" target="_blank">
LICENSE
</a>
<ImageRow images={[
{src: "img/emoji/mutant/8_ball.svg", name: "8_ball.svg"},
{src: "img/emoji/mutant/alien.svg", name: "alien.svg"},
{src: "img/emoji/mutant/american_football.svg", name: "american_football.svg"},
{src: "img/emoji/mutant/arms_in_the_air.svg", name: "arms_in_the_air.svg"},
{src: "img/emoji/mutant/artist.svg", name: "artist.svg"},
{src: "img/emoji/mutant/astronaut.svg", name: "astronaut.svg"},
{src: "img/emoji/mutant/back_of_hand_clw.svg", name: "back_of_hand_clw.svg"},
{src: "img/emoji/mutant/back_of_hand_hoof.svg", name: "back_of_hand_hoof.svg"},
{src: "img/emoji/mutant/back_of_hand_paw.svg", name: "back_of_hand_paw.svg"},
{src: "img/emoji/mutant/baseball.svg", name: "baseball.svg"},
{src: "img/emoji/mutant/basketball.svg", name: "basketball.svg"},
{src: "img/emoji/mutant/blep.svg", name: "blep.svg"},
{src: "img/emoji/mutant/bow_b3.svg", name: "bow_b3.svg"},
{src: "img/emoji/mutant/cat_crying.svg", name: "cat_crying.svg"},
{src: "img/emoji/mutant/cat_devious.svg", name: "cat_devious.svg"},
{src: "img/emoji/mutant/cat_grin.svg", name: "cat_grin.svg"},
{src: "img/emoji/mutant/cat_heart_eyes.svg", name: "cat_heart_eyes.svg"},
{src: "img/emoji/mutant/cat_joy.svg", name: "cat_joy.svg"},
{src: "img/emoji/mutant/cat_kiss.svg", name: "cat_kiss.svg"},
{src: "img/emoji/mutant/cat_pouting.svg", name: "cat_pouting.svg"},
{src: "img/emoji/mutant/cat_scream.svg", name: "cat_scream.svg"},
{src: "img/emoji/mutant/cat_smile.svg", name: "cat_smile.svg"},
{src: "img/emoji/mutant/chef.svg", name: "chef.svg"},
{src: "img/emoji/mutant/detective.svg", name: "detective.svg"},
{src: "img/emoji/mutant/ear.svg", name: "ear.svg"},
{src: "img/emoji/mutant/eye.svg", name: "eye.svg"},
{src: "img/emoji/mutant/eyes.svg", name: "eyes.svg"},
{src: "img/emoji/mutant/facepalm.svg", name: "facepalm.svg"},
{src: "img/emoji/mutant/football.svg", name: "football.svg"},
{src: "img/emoji/mutant/ghost.svg", name: "ghost.svg"},
{src: "img/emoji/mutant/grumpy_block.svg", name: "grumpy_block.svg"},
{src: "img/emoji/mutant/hot_shit.svg", name: "hot_shit.svg"},
{src: "img/emoji/mutant/jack_o_lantern.svg", name: "jack_o_lantern.svg"},
{src: "img/emoji/mutant/long_pointed_ear.svg", name: "long_pointed_ear.svg"},
{src: "img/emoji/mutant/mechanical_arm.svg", name: "mechanical_arm.svg"},
{src: "img/emoji/mutant/no_good.svg", name: "no_good.svg"},
{src: "img/emoji/mutant/office_worker.svg", name: "office_worker.svg"},
{src: "img/emoji/mutant/ok_gesture.svg", name: "ok_gesture.svg"},
{src: "img/emoji/mutant/person_frowning.svg", name: "person_frowning.svg"},
{src: "img/emoji/mutant/raising_hand.svg", name: "raising_hand.svg"},
{src: "img/emoji/mutant/robot.svg", name: "robot.svg"},
{src: "img/emoji/mutant/shrug.svg", name: "shrug.svg"},
{src: "img/emoji/mutant/singer.svg", name: "singer.svg"},
{src: "img/emoji/mutant/skull.svg", name: "skull.svg"},
{src: "img/emoji/mutant/skull_and_crossbones.svg", name: "skull_and_crossbones.svg"},
{src: "img/emoji/mutant/softball.svg", name: "softball.svg"},
{src: "img/emoji/mutant/student.svg", name: "student.svg"},
{src: "img/emoji/mutant/studio_microphone.svg", name: "studio_microphone.svg"},
{src: "img/emoji/mutant/technologist.svg", name: "technologist.svg"},
{src: "img/emoji/mutant/tennis.svg", name: "tennis.svg"},
{src: "img/emoji/mutant/volleyball.svg", name: "volleyball.svg"},
]}/>
</Story>

View File

@@ -1,51 +1,16 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
import Logo from '../../assets/images/logo.svg';
import FediverseColor from '../../public/img/fediverse-color.png';
import FediverseBlack from '../../public/img/fediverse-black.png';
import Moderator from '../../assets/images/moderator.svg';
import IndieAuth from '../../public/img/indieauth.png';
import IsBot from '../../assets/images/bot.svg';
<Meta title="owncast/Style Guide/Images+Icons" />
export const images = [
{
src: Logo,
name: 'Logo',
},
];
<Meta title="owncast/Frontend Assets/Images" />
# Images
## TODO: Determine the icon style/images for v2 of the web UI.
<ImageRow images={[
{src: "img/fediverse-black.png", name: "fediverse-black.png"},
{src: "img/fediverse-color.png", name: "fediverse-color.png"},
{src: "img/follow.svg", name: "follow.svg"},
{src: "img/indieauth.png", name: "indieauth.png"},
{src: "img/like.svg", name: "like.svg"},
{src: "img/repost.svg", name: "repost.svg"},
]}/>
<ImageRow images={images} />
## App Icons
export const icons = [
{
src: FediverseColor,
name: 'Fediverse Color',
},
{
src: FediverseBlack,
name: 'Fediverse Black',
},
{
src: Moderator,
name: 'Moderator',
},
{
src: IndieAuth,
name: 'IndieAuth',
},
{
src: IsBot,
name: 'Bot Flag',
},
];
<ImageRow images={icons} />

View File

@@ -0,0 +1,20 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Project Assets/Logos &amp; Graphics" />
# Logos &amp; Graphics
<ImageRow images={[
{src: "project/header.png", name: "header.png"},
{src: "project/kiss-cut-stickers-5.5x5.5-default-60874a6c11849.png", name: "kiss-cut-stickers-5.5x5.5-default-60874a6c11849.png"},
{src: "project/logo-glare-outlined.png", name: "logo-glare-outlined.png"},
{src: "project/logo-glare-vector.svg", name: "logo-glare-vector.svg"},
{src: "project/logo-noglare-vector.svg", name: "logo-noglare-vector.svg"},
{src: "project/logo-translucent-grey.svg", name: "logo-translucent-grey.svg"},
{src: "project/logo-white.svg", name: "logo-white.svg"},
{src: "project/owncast-background.png", name: "owncast-background.png"},
{src: "project/owncast-browser-mobile.png", name: "owncast-browser-mobile.png"},
{src: "project/sticker-bigtech-alt.png", name: "sticker-bigtech-alt.png"},
]}/>

View File

@@ -0,0 +1,91 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/Product Definition" />
# Owncast Product Definition
## Why
By defining the goals and target user bases we have something stable to guide decisions, features, conversations and keep clarity around what is being built.
While these definitions and lists should not be seen as exhaustive, in theory, once this is seen as "complete" there should be few, if any changes, as that would note a large change in direction and goals.
[TOC]
## Vision
> The out-of-the-box personal broadcast platform for DIY streamers and integrators.
## Primary Goals
- Useful out of the box.
- Fast to get running.
- Self-contained.
- An alternative, not a competitor.
- For individuals, not service providers.
- Easy to integrate into other projects/products.
- Low barrier to entry.
- Empowering.
- Customizable and hackable.
## Primary Users
### The DIY Streamer
An individual who is streaming as a hobby, a project, or is moving their audience from an existing streaming platform.
**Needs**:
- Security/ownership of their own stream.
- Building an independent space.
- Personalization.
- Tools to manage a relationship with their audience.
**Pains**:
- Kicked off other streaming services.
- Feeling of inequality or bias.
- Their content has low visibility.
- Platform rules do not align with them.
- Do not agree with the forced ads, tracking and analytics.
### The Integrator
An individual or organization that has existing content, products or platforms that they want to add live streaming functionality to.
**Needs**:
- Broadcasting without censorship.
- Full ownership of their brand.
- Embedding and 3rd party playback.
- Support private or invite-only streams.
- Independence.
**Pains**:
- Censorship.
- Rules.
- Ads.
- Risk of losing viewers from competitors and distractions.
**Desires**:
- Hosting events.
- Running their own broadcasting service.
## Secondary Users
### The Viewer
An audience member that is often, but not always, taking part in chat.
**Needs**:
- To watch high quality video.
- Ways to interact with the streamer. Chat, memes, emoji.
- Calls to actions, links, next steps.
**Pains**:
- Understanding the interface and knowing they're in the correct place.

View File

@@ -0,0 +1,41 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Frontend Assets/Social Platform Images" />
# Social Platform Images
<ImageRow images={[
{src: "img/platformlogos/bandcamp.svg", name: "bandcamp.svg"},
{src: "img/platformlogos/default.svg", name: "default.svg"},
{src: "img/platformlogos/discord.svg", name: "discord.svg"},
{src: "img/platformlogos/donate.svg", name: "donate.svg"},
{src: "img/platformlogos/facebook.svg", name: "facebook.svg"},
{src: "img/platformlogos/fediverse.svg", name: "fediverse.svg"},
{src: "img/platformlogos/follow.svg", name: "follow.svg"},
{src: "img/platformlogos/github.svg", name: "github.svg"},
{src: "img/platformlogos/gitlab.svg", name: "gitlab.svg"},
{src: "img/platformlogos/google.svg", name: "google.svg"},
{src: "img/platformlogos/instagram.svg", name: "instagram.svg"},
{src: "img/platformlogos/keyoxide.png", name: "keyoxide.png"},
{src: "img/platformlogos/ko-fi.svg", name: "ko-fi.svg"},
{src: "img/platformlogos/lbry.svg", name: "lbry.svg"},
{src: "img/platformlogos/liberapay.svg", name: "liberapay.svg"},
{src: "img/platformlogos/link.svg", name: "link.svg"},
{src: "img/platformlogos/linkedin.svg", name: "linkedin.svg"},
{src: "img/platformlogos/mastodon.svg", name: "mastodon.svg"},
{src: "img/platformlogos/matrix.svg", name: "matrix.svg"},
{src: "img/platformlogos/odysee.svg", name: "odysee.svg"},
{src: "img/platformlogos/patreon.svg", name: "patreon.svg"},
{src: "img/platformlogos/paypal.svg", name: "paypal.svg"},
{src: "img/platformlogos/snapchat.svg", name: "snapchat.svg"},
{src: "img/platformlogos/soundcloud.svg", name: "soundcloud.svg"},
{src: "img/platformlogos/spotify.svg", name: "spotify.svg"},
{src: "img/platformlogos/steam.svg", name: "steam.svg"},
{src: "img/platformlogos/tiktok.svg", name: "tiktok.svg"},
{src: "img/platformlogos/twitch.svg", name: "twitch.svg"},
{src: "img/platformlogos/twitter.svg", name: "twitter.svg"},
{src: "img/platformlogos/xmpp.svg", name: "xmpp.svg"},
{src: "img/platformlogos/youtube.svg", name: "youtube.svg"},
]}/>

View File

@@ -0,0 +1,23 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Project Assets/T-Shirt" />
# T-shirt
<ImageRow images={[
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-back-60873dde52297.png", name: "all-over-print-mens-crew-neck-t-shirt-white-back-60873dde52297.png"},
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-back-60873dde523ae.png", name: "all-over-print-mens-crew-neck-t-shirt-white-back-60873dde523ae.png"},
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-back-60873dde524ca.png", name: "all-over-print-mens-crew-neck-t-shirt-white-back-60873dde524ca.png"},
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-front-60873dde51eb3.png", name: "all-over-print-mens-crew-neck-t-shirt-white-front-60873dde51eb3.png"},
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-front-60873dde52064.png", name: "all-over-print-mens-crew-neck-t-shirt-white-front-60873dde52064.png"},
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-left-60873dde525e2.png", name: "all-over-print-mens-crew-neck-t-shirt-white-left-60873dde525e2.png"},
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-right-60873dde52184.png", name: "all-over-print-mens-crew-neck-t-shirt-white-right-60873dde52184.png"},
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-back-6087418b62999.png", name: "all-over-print-womens-crew-neck-t-shirt-white-back-6087418b62999.png"},
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-back-6087418b62aa4.png", name: "all-over-print-womens-crew-neck-t-shirt-white-back-6087418b62aa4.png"},
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-front-6087418b626d5.png", name: "all-over-print-womens-crew-neck-t-shirt-white-front-6087418b626d5.png"},
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-front-6087418b62878.png", name: "all-over-print-womens-crew-neck-t-shirt-white-front-6087418b62878.png"},
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-left-6087418b62b91.png", name: "all-over-print-womens-crew-neck-t-shirt-white-left-6087418b62b91.png"},
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-right-6087418b62c88.png", name: "all-over-print-womens-crew-neck-t-shirt-white-right-6087418b62c88.png"},
]}/>

View File

@@ -1,6 +1,6 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Style Guide/Typography" />
<Meta title="owncast/Styles/Typography" />
## Body

View File

@@ -0,0 +1,78 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/Building Frontend Components" />
# How we develop components
This document outlines how we develop the components for the Owncast Web UI.
You should use this document as a guide when making changes to existing components, and adding new ones.
Working with the same development process help keep the project maintainable.
## What are components
A component in React is a custom HTML element. They're included in the DOM just like regular elements `<ChatBox /`>.
## Functional Components
In react, there's two ways to write a component: there's Class-based Components, and Functional Components.
Class-based is older and has fallen out of favor.
Functional Components are the new standard and you'll find them in most React projects written today.
See the [React Functional Component docs](https://reactjs.org/docs/components-and-props.html) for more info.
### How we write Functional Components
We've defined a pattern for how we write Functional Components in the Owncast Web UI.
There's a few ways to to write Functional Components that are common, so defining a standard helps keep this project readable and consistent.
The pattern we've settled on is:
**For stateless components:**
```tsx
export type MyNewButtonProps = {
label: string;
onClick: () => void;
};
export const MyNewButton: FC<MyNewButtonProps> = ({ label, onClick }) => (
<button onClick={onCLick}>{label}</button>
);
```
**For stateful components:**
```tsx
export type MyNewButtonProps = {
label: string;
onClick: () => void;
};
export const MyNewButton: FC<MyNewButtonProps> = ({ label, onClick }) => {
// do something, then call the onClick fn. e.g.:
const handleClick = useCallback(() => {
alert(label);
onClick && onClick();
}, [label, onClick]);
return <button onClick={onCLick}>{label}</button>;
};
```
### Rationale
Since there's a lot of ways to create components, settling on one pattern helps maintain readability.
But why _this_ style?
See the discussion on the PR that introduced this pattern: [#2082](https://github.com/owncast/owncast/pull/2082).
## Storybook
We use [Storybook](https://storybook.js.org/) to create a component library where we can see and interact with each component.
Make sure to include a `.stories.tsx` file with each (exported) component you create, and to update the stories file when making changes to existing components.
You can run the Storybook server locally with `npm run storybook`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="100%"
height="100%"
viewBox="0 0 96 105"
version="1.1"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
class="svg-logo-solid"
id="svg125"
sodipodi:docname="logo-translucent-grey.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs129" /><sodipodi:namedview
id="namedview127"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="7.4571429"
inkscape:cx="19.444444"
inkscape:cy="68.591954"
inkscape:window-width="1916"
inkscape:window-height="1055"
inkscape:window-x="0"
inkscape:window-y="21"
inkscape:window-maximized="1"
inkscape:current-layer="svg125" />
<g
transform="matrix(1.04457,0,0,1.04457,-0.742448,-0.0626735)"
id="g123"
style="fill:#4699ff;opacity:0.04;fill-opacity:1">
<g
id="g105"
style="fill:#4699ff;fill-opacity:1">
<path
d="M91.5,75.35C92.533,72.55 92.583,70 91.65,67.7C90.783,65.567 89.117,63.767 86.65,62.3C84.35,60.967 81.567,60 78.3,59.4C75.333,58.867 72.1,58.633 68.6,58.7C65.233,58.8 61.967,59.167 58.8,59.8C55.767,60.433 53.1,61.233 50.8,62.2C48.533,63.167 46.767,64.217 45.5,65.35C44.233,66.55 43.567,67.783 43.5,69.05C43.4,70.55 44.167,72.167 45.8,73.9C47.3,75.5 49.4,77.067 52.1,78.6C54.8,80.133 57.783,81.45 61.05,82.55C64.55,83.717 68,84.467 71.4,84.8C73.6,85 75.65,85.033 77.55,84.9C79.617,84.7 81.533,84.267 83.3,83.6C85.2,82.867 86.817,81.85 88.15,80.55C89.65,79.117 90.767,77.383 91.5,75.35M70.6,67.5C71.733,68.1 72.567,68.833 73.1,69.7C73.633,70.667 73.75,71.767 73.45,73C73.217,73.867 72.833,74.617 72.3,75.25C71.8,75.817 71.133,76.267 70.3,76.6C69.6,76.9 68.75,77.117 67.75,77.25C66.783,77.35 65.817,77.367 64.85,77.3C63.15,77.2 61.283,76.867 59.25,76.3C57.483,75.767 55.783,75.1 54.15,74.3C52.65,73.567 51.417,72.8 50.45,72C49.517,71.167 49.067,70.433 49.1,69.8C49.167,69.267 49.55,68.75 50.25,68.25C50.95,67.783 51.917,67.367 53.15,67C54.383,66.6 55.75,66.3 57.25,66.1C58.95,65.9 60.567,65.8 62.1,65.8C63.8,65.833 65.333,65.967 66.7,66.2C68.167,66.5 69.467,66.933 70.6,67.5Z"
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
id="path103" />
</g>
<g
id="g109"
style="fill:#4699ff;fill-opacity:1">
<path
d="M66.6,15.05C66.467,11.45 65.567,8.45 63.9,6.05C62.133,3.417 59.533,1.617 56.1,0.65C55.333,0.417 54.517,0.25 53.65,0.15C52.883,0.05 52.1,0.033 51.3,0.1C50.567,0.1 49.833,0.183 49.1,0.35C48.467,0.483 47.767,0.7 47,1C44.533,1.967 42.3,3.667 40.3,6.1C38.433,8.3 36.833,11.033 35.5,14.3C34.333,17.067 33.4,20.1 32.7,23.4C32.033,26.5 31.583,29.65 31.35,32.85C31.15,35.75 31.133,38.533 31.3,41.2C31.5,43.833 31.867,46.217 32.4,48.35C33.467,52.717 35.1,55.4 37.3,56.4C37.5,56.5 37.7,56.583 37.9,56.65L39.2,56.85C39.367,56.85 39.617,56.833 39.95,56.8C41.35,56.667 42.933,56.083 44.7,55.05C46.4,54.017 48.183,52.6 50.05,50.8C52.05,48.867 53.983,46.617 55.85,44.05C57.817,41.383 59.567,38.567 61.1,35.6C62.9,32.1 64.283,28.667 65.25,25.3C66.25,21.6 66.7,18.183 66.6,15.05M47.55,23.15C47.883,23.217 48.167,23.3 48.4,23.4C51.1,24.333 52.483,26.483 52.55,29.85C52.583,32.617 51.733,35.8 50,39.4C48.567,42.4 46.85,45.033 44.85,47.3C42.983,49.433 41.417,50.567 40.15,50.7L39.9,50.75L39.45,50.7L39.2,50.6C38.267,50.167 37.617,48.75 37.25,46.35C36.883,43.917 36.9,41.133 37.3,38C37.733,34.5 38.55,31.433 39.75,28.8C41.183,25.667 42.95,23.817 45.05,23.25C45.417,23.15 45.683,23.1 45.85,23.1C46.117,23.067 46.383,23.05 46.65,23.05C46.917,23.05 47.217,23.083 47.55,23.15Z"
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
id="path107" />
</g>
<g
id="g113"
style="fill:#4699ff;fill-opacity:1">
<path
d="M2.7,33.6C2.3,34.133 1.967,34.717 1.7,35.35C1.4,36.117 1.183,36.9 1.05,37.7C0.35,40.967 0.733,44.133 2.2,47.2C3.4,49.733 5.333,52.117 8,54.35C10.367,56.317 13.033,57.917 16,59.15C19,60.383 21.617,60.95 23.85,60.85C24.283,60.85 24.75,60.8 25.25,60.7C25.75,60.6 26.167,60.467 26.5,60.3C26.833,60.133 27.15,59.917 27.45,59.65C27.75,59.383 27.983,59.083 28.15,58.75C28.95,57.217 28.733,54.85 27.5,51.65C26.233,48.55 24.317,45.367 21.75,42.1C19.083,38.7 16.3,36.017 13.4,34.05C10.267,31.95 7.617,31.167 5.45,31.7C4.917,31.833 4.417,32.067 3.95,32.4C3.483,32.7 3.067,33.1 2.7,33.6M10.1,43.55C10.267,43.25 10.433,43.017 10.6,42.85C10.767,42.683 10.967,42.533 11.2,42.4C11.467,42.3 11.7,42.233 11.9,42.2C12.967,42 14.317,42.467 15.95,43.6C17.417,44.567 18.883,45.933 20.35,47.7C21.683,49.3 22.75,50.867 23.55,52.4C24.317,53.967 24.55,55.067 24.25,55.7C24.183,55.833 24.1,55.933 24,56C23.9,56.133 23.783,56.217 23.65,56.25C23.583,56.317 23.45,56.367 23.25,56.4L22.7,56.5C21.633,56.567 20.25,56.267 18.55,55.6C16.883,54.933 15.317,54.05 13.85,52.95C12.283,51.783 11.117,50.517 10.35,49.15C9.483,47.583 9.283,46.017 9.75,44.45C9.85,44.117 9.967,43.817 10.1,43.55Z"
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
id="path111" />
</g>
<g
id="g117"
style="fill:#4699ff;fill-opacity:1">
<path
d="M34.95,74.2L34.75,74.2C33.717,74.167 32.767,74.517 31.9,75.25C31.1,75.95 30.417,76.95 29.85,78.25C29.35,79.417 29,80.733 28.8,82.2C28.6,83.667 28.567,85.15 28.7,86.65C28.967,89.817 29.9,92.5 31.5,94.7C33.367,97.233 35.967,98.9 39.3,99.7L39.4,99.7L39.7,99.8L39.85,99.8C43.483,100.5 45.917,99.817 47.15,97.75C47.717,96.783 48,95.55 48,94.05C47.967,92.617 47.7,91.05 47.2,89.35C46.7,87.617 46,85.883 45.1,84.15C44.2,82.383 43.183,80.783 42.05,79.35C40.85,77.85 39.65,76.65 38.45,75.75C37.183,74.817 36.017,74.3 34.95,74.2M33.55,80.4C34.083,78.933 34.767,78.233 35.6,78.3L35.65,78.3C36.483,78.4 37.467,79.267 38.6,80.9C39.733,82.533 40.583,84.25 41.15,86.05C41.783,88.017 41.917,89.583 41.55,90.75C41.117,91.983 40.05,92.483 38.35,92.25L38.3,92.25L38.25,92.2L38.1,92.2C36.433,91.867 35.15,91 34.25,89.6C33.483,88.333 33.05,86.8 32.95,85C32.85,83.233 33.05,81.7 33.55,80.4Z"
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
id="path115" />
</g>
<g
id="g121"
style="fill:#4699ff;fill-opacity:1">
<path
d="M22.7,69.65C22.4,69.417 22.033,69.217 21.6,69.05C21.167,68.883 20.717,68.767 20.25,68.7C19.817,68.6 19.35,68.533 18.85,68.5C17.417,68.467 16.017,68.683 14.65,69.15C13.317,69.583 12.233,70.233 11.4,71.1C10.567,72.033 10.167,73.067 10.2,74.2C10.233,75.433 10.817,76.767 11.95,78.2C12.25,78.567 12.617,78.967 13.05,79.4C13.383,79.733 13.767,80.033 14.2,80.3C14.533,80.5 14.9,80.683 15.3,80.85C15.767,81.017 16.133,81.1 16.4,81.1C17.6,81.267 18.767,81.017 19.9,80.35C21,79.717 21.95,78.817 22.75,77.65C23.583,76.45 24.1,75.217 24.3,73.95C24.5,72.55 24.25,71.4 23.55,70.5C23.283,70.167 23,69.883 22.7,69.65M21.7,71.7C22,72.1 22.067,72.633 21.9,73.3C21.767,73.933 21.467,74.583 21,75.25C20.533,75.883 20,76.383 19.4,76.75C18.767,77.15 18.15,77.317 17.55,77.25L17,77.15C16.8,77.083 16.617,76.983 16.45,76.85C16.317,76.783 16.133,76.65 15.9,76.45C15.767,76.317 15.6,76.133 15.4,75.9C14.8,75.133 14.567,74.433 14.7,73.8C14.767,73.233 15.117,72.733 15.75,72.3C16.317,71.9 17,71.6 17.8,71.4C18.6,71.2 19.367,71.117 20.1,71.15L20.65,71.2L21.1,71.3C21.233,71.367 21.35,71.433 21.45,71.5L21.7,71.7Z"
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
id="path119" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,19 @@
<svg width="100%" height="100%" viewBox="0 0 96 105" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" class="svg-logo-solid">
<g transform="matrix(1.04457,0,0,1.04457,-0.742448,-0.0626735)">
<g>
<path d="M91.5,75.35C92.533,72.55 92.583,70 91.65,67.7C90.783,65.567 89.117,63.767 86.65,62.3C84.35,60.967 81.567,60 78.3,59.4C75.333,58.867 72.1,58.633 68.6,58.7C65.233,58.8 61.967,59.167 58.8,59.8C55.767,60.433 53.1,61.233 50.8,62.2C48.533,63.167 46.767,64.217 45.5,65.35C44.233,66.55 43.567,67.783 43.5,69.05C43.4,70.55 44.167,72.167 45.8,73.9C47.3,75.5 49.4,77.067 52.1,78.6C54.8,80.133 57.783,81.45 61.05,82.55C64.55,83.717 68,84.467 71.4,84.8C73.6,85 75.65,85.033 77.55,84.9C79.617,84.7 81.533,84.267 83.3,83.6C85.2,82.867 86.817,81.85 88.15,80.55C89.65,79.117 90.767,77.383 91.5,75.35M70.6,67.5C71.733,68.1 72.567,68.833 73.1,69.7C73.633,70.667 73.75,71.767 73.45,73C73.217,73.867 72.833,74.617 72.3,75.25C71.8,75.817 71.133,76.267 70.3,76.6C69.6,76.9 68.75,77.117 67.75,77.25C66.783,77.35 65.817,77.367 64.85,77.3C63.15,77.2 61.283,76.867 59.25,76.3C57.483,75.767 55.783,75.1 54.15,74.3C52.65,73.567 51.417,72.8 50.45,72C49.517,71.167 49.067,70.433 49.1,69.8C49.167,69.267 49.55,68.75 50.25,68.25C50.95,67.783 51.917,67.367 53.15,67C54.383,66.6 55.75,66.3 57.25,66.1C58.95,65.9 60.567,65.8 62.1,65.8C63.8,65.833 65.333,65.967 66.7,66.2C68.167,66.5 69.467,66.933 70.6,67.5Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g>
<path d="M66.6,15.05C66.467,11.45 65.567,8.45 63.9,6.05C62.133,3.417 59.533,1.617 56.1,0.65C55.333,0.417 54.517,0.25 53.65,0.15C52.883,0.05 52.1,0.033 51.3,0.1C50.567,0.1 49.833,0.183 49.1,0.35C48.467,0.483 47.767,0.7 47,1C44.533,1.967 42.3,3.667 40.3,6.1C38.433,8.3 36.833,11.033 35.5,14.3C34.333,17.067 33.4,20.1 32.7,23.4C32.033,26.5 31.583,29.65 31.35,32.85C31.15,35.75 31.133,38.533 31.3,41.2C31.5,43.833 31.867,46.217 32.4,48.35C33.467,52.717 35.1,55.4 37.3,56.4C37.5,56.5 37.7,56.583 37.9,56.65L39.2,56.85C39.367,56.85 39.617,56.833 39.95,56.8C41.35,56.667 42.933,56.083 44.7,55.05C46.4,54.017 48.183,52.6 50.05,50.8C52.05,48.867 53.983,46.617 55.85,44.05C57.817,41.383 59.567,38.567 61.1,35.6C62.9,32.1 64.283,28.667 65.25,25.3C66.25,21.6 66.7,18.183 66.6,15.05M47.55,23.15C47.883,23.217 48.167,23.3 48.4,23.4C51.1,24.333 52.483,26.483 52.55,29.85C52.583,32.617 51.733,35.8 50,39.4C48.567,42.4 46.85,45.033 44.85,47.3C42.983,49.433 41.417,50.567 40.15,50.7L39.9,50.75L39.45,50.7L39.2,50.6C38.267,50.167 37.617,48.75 37.25,46.35C36.883,43.917 36.9,41.133 37.3,38C37.733,34.5 38.55,31.433 39.75,28.8C41.183,25.667 42.95,23.817 45.05,23.25C45.417,23.15 45.683,23.1 45.85,23.1C46.117,23.067 46.383,23.05 46.65,23.05C46.917,23.05 47.217,23.083 47.55,23.15Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g>
<path d="M2.7,33.6C2.3,34.133 1.967,34.717 1.7,35.35C1.4,36.117 1.183,36.9 1.05,37.7C0.35,40.967 0.733,44.133 2.2,47.2C3.4,49.733 5.333,52.117 8,54.35C10.367,56.317 13.033,57.917 16,59.15C19,60.383 21.617,60.95 23.85,60.85C24.283,60.85 24.75,60.8 25.25,60.7C25.75,60.6 26.167,60.467 26.5,60.3C26.833,60.133 27.15,59.917 27.45,59.65C27.75,59.383 27.983,59.083 28.15,58.75C28.95,57.217 28.733,54.85 27.5,51.65C26.233,48.55 24.317,45.367 21.75,42.1C19.083,38.7 16.3,36.017 13.4,34.05C10.267,31.95 7.617,31.167 5.45,31.7C4.917,31.833 4.417,32.067 3.95,32.4C3.483,32.7 3.067,33.1 2.7,33.6M10.1,43.55C10.267,43.25 10.433,43.017 10.6,42.85C10.767,42.683 10.967,42.533 11.2,42.4C11.467,42.3 11.7,42.233 11.9,42.2C12.967,42 14.317,42.467 15.95,43.6C17.417,44.567 18.883,45.933 20.35,47.7C21.683,49.3 22.75,50.867 23.55,52.4C24.317,53.967 24.55,55.067 24.25,55.7C24.183,55.833 24.1,55.933 24,56C23.9,56.133 23.783,56.217 23.65,56.25C23.583,56.317 23.45,56.367 23.25,56.4L22.7,56.5C21.633,56.567 20.25,56.267 18.55,55.6C16.883,54.933 15.317,54.05 13.85,52.95C12.283,51.783 11.117,50.517 10.35,49.15C9.483,47.583 9.283,46.017 9.75,44.45C9.85,44.117 9.967,43.817 10.1,43.55Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g>
<path d="M34.95,74.2L34.75,74.2C33.717,74.167 32.767,74.517 31.9,75.25C31.1,75.95 30.417,76.95 29.85,78.25C29.35,79.417 29,80.733 28.8,82.2C28.6,83.667 28.567,85.15 28.7,86.65C28.967,89.817 29.9,92.5 31.5,94.7C33.367,97.233 35.967,98.9 39.3,99.7L39.4,99.7L39.7,99.8L39.85,99.8C43.483,100.5 45.917,99.817 47.15,97.75C47.717,96.783 48,95.55 48,94.05C47.967,92.617 47.7,91.05 47.2,89.35C46.7,87.617 46,85.883 45.1,84.15C44.2,82.383 43.183,80.783 42.05,79.35C40.85,77.85 39.65,76.65 38.45,75.75C37.183,74.817 36.017,74.3 34.95,74.2M33.55,80.4C34.083,78.933 34.767,78.233 35.6,78.3L35.65,78.3C36.483,78.4 37.467,79.267 38.6,80.9C39.733,82.533 40.583,84.25 41.15,86.05C41.783,88.017 41.917,89.583 41.55,90.75C41.117,91.983 40.05,92.483 38.35,92.25L38.3,92.25L38.25,92.2L38.1,92.2C36.433,91.867 35.15,91 34.25,89.6C33.483,88.333 33.05,86.8 32.95,85C32.85,83.233 33.05,81.7 33.55,80.4Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g>
<path d="M22.7,69.65C22.4,69.417 22.033,69.217 21.6,69.05C21.167,68.883 20.717,68.767 20.25,68.7C19.817,68.6 19.35,68.533 18.85,68.5C17.417,68.467 16.017,68.683 14.65,69.15C13.317,69.583 12.233,70.233 11.4,71.1C10.567,72.033 10.167,73.067 10.2,74.2C10.233,75.433 10.817,76.767 11.95,78.2C12.25,78.567 12.617,78.967 13.05,79.4C13.383,79.733 13.767,80.033 14.2,80.3C14.533,80.5 14.9,80.683 15.3,80.85C15.767,81.017 16.133,81.1 16.4,81.1C17.6,81.267 18.767,81.017 19.9,80.35C21,79.717 21.95,78.817 22.75,77.65C23.583,76.45 24.1,75.217 24.3,73.95C24.5,72.55 24.25,71.4 23.55,70.5C23.283,70.167 23,69.883 22.7,69.65M21.7,71.7C22,72.1 22.067,72.633 21.9,73.3C21.767,73.933 21.467,74.583 21,75.25C20.533,75.883 20,76.383 19.4,76.75C18.767,77.15 18.15,77.317 17.55,77.25L17,77.15C16.8,77.083 16.617,76.983 16.45,76.85C16.317,76.783 16.133,76.65 15.9,76.45C15.767,76.317 15.6,76.133 15.4,75.9C14.8,75.133 14.567,74.433 14.7,73.8C14.767,73.233 15.117,72.733 15.75,72.3C16.317,71.9 17,71.6 17.8,71.4C18.6,71.2 19.367,71.117 20.1,71.15L20.65,71.2L21.1,71.3C21.233,71.367 21.35,71.433 21.45,71.5L21.7,71.7Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

View File

@@ -1,11 +1,8 @@
import React from 'react';
import { DocsContainer as BaseContainer } from '@storybook/addon-docs';
import { useDarkMode } from 'storybook-dark-mode';
import { themes } from '@storybook/theming';
export const DocsContainer = ({ children, context }) => {
const dark = useDarkMode();
return (
<BaseContainer
context={{
@@ -18,7 +15,7 @@ export const DocsContainer = ({ children, context }) => {
...storyContext?.parameters,
docs: {
...storyContext?.parameters?.docs,
theme: dark ? themes.dark : themes.light,
theme: themes.light,
},
},
};

View File

@@ -0,0 +1,5 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
<Meta title="owncast/Documentation/{{title}}" />
{{content}}

View File

@@ -0,0 +1,25 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="owncast/Frontend Assets/Emoji" />
# Built-in Custom Emoji
{{#each emojiCollections}}
## {{capitalize this.name}}
<Story
name="{{capitalize this.name}}"
>
<a href="img/emoji/{{this.name}}/LICENSE.md" target="_blank">
LICENSE
</a>
<ImageRow images={[
{{#each this.images}}
{src: "{{this.src}}", name: "{{this.name}}"},
{{/each}}
]}/>
</Story>
{{/each}}

View File

@@ -0,0 +1,12 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Image, ImageRow } from './ImageAsset';
<Meta title="{{category}}" />
# {{capitalize title}}
<ImageRow images={[
{{#each images}}
{src: "{{this.src}}", name: "{{this.name}}"},
{{/each}}
]}/>

View File

@@ -0,0 +1,34 @@
import fs from 'fs';
import handlebars from 'handlebars';
const template = fs.readFileSync('./Document.stories.mdx', 'utf8');
let t = handlebars.compile(template, { noEscape: true });
const documents = [
{
title: 'Product Definition',
name: 'ProductDefinition',
path: '../../../docs/product-definition.md',
},
{ title: 'Design', name: 'Design', path: '../../../.design/DESIGN.md' },
{
title: 'Building Frontend Components',
name: 'WebComponents',
path: '../../../web/components/_COMPONENT_HOW_TO.md',
},
{
title: 'Get Started with Owncast Development',
name: 'Development',
path: '/tmp/development.md',
},
];
documents.forEach(doc => {
if (!fs.existsSync(doc.path)) {
return;
}
const document = fs.readFileSync(doc.path, 'utf8');
const output = t({ name: doc.name, title: doc.title, content: document });
fs.writeFileSync(`../stories-category-doc-pages/${doc.name}.stories.mdx`, output);
});

View File

@@ -0,0 +1,37 @@
import fs from 'fs';
import path from 'path';
import { readdirSync } from 'fs';
import handlebars from 'handlebars';
handlebars.registerHelper('capitalize', function (str) {
return str.charAt(0).toUpperCase() + str.slice(1);
});
function getDirectories(path) {
return fs.readdirSync(path).filter(function (file) {
return fs.statSync(path + '/' + file).isDirectory();
});
}
const emojiDir = path.resolve('../../../static/img/emoji');
const emojiCollectionDirs = getDirectories(emojiDir).map(dir => {
return dir;
});
let emojiCollections = {};
emojiCollectionDirs.forEach(collection => {
const emojiCollection = readdirSync(path.resolve(emojiDir, collection))
.filter(f => f.toLowerCase() !== 'license.md')
.map(emoji => {
return { name: emoji, src: `img/emoji/${collection}/${emoji}` };
});
emojiCollections[collection] = { name: collection, images: emojiCollection };
});
const template = fs.readFileSync('./Emoji.stories.mdx', 'utf8');
let t = handlebars.compile(template);
let output = t({ emojiCollections });
console.log(output);

View File

@@ -0,0 +1,36 @@
import fs from 'fs';
import path, { resolve } from 'path';
import { readdirSync, lstatSync } from 'fs';
import handlebars from 'handlebars';
handlebars.registerHelper('capitalize', function (str) {
return str.charAt(0).toUpperCase() + str.slice(1);
});
const args = process.argv;
const dir = args[2];
const title = args[3];
const category = args[4];
const publicPath = args[5];
if (args.length < 6) {
console.error('Usage: generate-image-story.mjs <dir> <title> <category> <webpublicpath>');
process.exit(1);
}
const images = readdirSync(dir)
.map(img => {
const resolvedPath = path.resolve(dir, img);
if (lstatSync(resolvedPath).isDirectory()) {
return;
}
return { name: img, src: `${publicPath}/${img}` };
})
.filter(Boolean);
const template = fs.readFileSync('./Images.stories.mdx', 'utf8');
let t = handlebars.compile(template);
let output = t({ images, title, category });
console.log(output);

View File

@@ -0,0 +1,17 @@
#!/bin/sh
# Generate the custom Emoji story
node generate-emoji-story.mjs >../stories-category-doc-pages/Emoji.stories.mdx
# Generate stories out of documentation
# Pull down the doc about development
curl -s https://raw.githubusercontent.com/owncast/owncast.github.io/master/content/development.md >/tmp/development.md
node generate-document-stories.mjs
# Project image assets
node generate-image-story.mjs ../../public/img/ Images "owncast/Frontend Assets/Images" "img" >../stories-category-doc-pages/Images.stories.mdx
node generate-image-story.mjs ../../public/img/platformlogos/ "Social Platform Images" "owncast/Frontend Assets/Social Platform Images" "img/platformlogos" >../stories-category-doc-pages/SocialPlatformImages.stories.mdx
node generate-image-story.mjs ../story-assets/project/ "Logos & Graphics" "owncast/Project Assets/Logos & Graphics" "project" >../stories-category-doc-pages/LogosAndGraphics.stories.mdx
node generate-image-story.mjs ../story-assets/tshirt/ "T-shirt" "owncast/Project Assets/T-Shirt" "tshirt" >../stories-category-doc-pages/Tshirt.stories.mdx

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,13 +45,11 @@
}
.virtuoso::-webkit-scrollbar {
width: 5px;
height: auto;
background-color: var(--theme-color-components-chat-background);
display: none;
}
.virtuoso::-webkit-scrollbar-thumb {
background: var(--theme-color-components-scrollbar-thumb);
display: none;
}
.chatTextField {

View File

@@ -2,11 +2,8 @@
.chatModerationNotification {
background-color: var(--theme-background-primary);
color: var(--theme-color-components-chat-text);
margin: 5px;
border-radius: 15px;
border-color: rgba(0, 0, 0, 0.3);
border-width: 1px;
border-style: solid;
padding: 10px 10px;
@include flexCenter;

View File

@@ -7,6 +7,7 @@
rgb(83, 67, 130) 80%
);
margin: 5px;
margin-bottom: 10px;
border-radius: 5px;
border-width: 1px;
border-style: solid;
@@ -34,4 +35,13 @@
background-color: var(--theme-color-palette-12);
}
}
a {
color: var(--theme-color-palette-4);
:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-color: var(--theme-color-palette-15);
}
}
}

View File

@@ -6,9 +6,9 @@
bottom: 0px;
width: 100%;
padding: 4px 0.1vw;
padding: 0.6em;
overflow-x: hidden;
background-color: var(--theme-color-palette-3);
background-color: var(--theme-color-components-chat-background);
.inputWrap {
position: relative;
@@ -23,7 +23,6 @@
transition: box-shadow 90ms ease-in-out;
&:focus-within {
background-color: var(--theme-color-components-form-field-background);
// outline: 1px solid var(--theme-color-components-form-field-border);
box-shadow: inset 0px 0px 2px 2px var(--theme-color-palette-3);
}
}

View File

@@ -22,6 +22,7 @@ $p-size: 8px;
overflow: hidden;
overflow-wrap: anywhere;
font-weight: 500;
position: relative;
mark {
padding-left: 0.35em;
@@ -52,10 +53,16 @@ $p-size: 8px;
display: none;
top: 0;
right: 10px;
color: var(--theme-color-components-text-on-light);
& button:focus,
& button:active {
display: block !important;
}
button {
border-radius: var(--theme-rounded-corners);
opacity: 0.8;
}
}
&:hover .modMenuWrapper {

View File

@@ -5,6 +5,7 @@ import { Tooltip } from 'antd';
import { useRecoilValue } from 'recoil';
import dynamic from 'next/dynamic';
import { decodeHTML } from 'entities';
import linkifyHtml from 'linkify-html';
import styles from './ChatUserMessage.module.scss';
import { formatTimestamp } from './messageFmt';
import { ChatMessage } from '../../../interfaces/chat-message.model';
@@ -107,6 +108,8 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
})}
style={{ borderColor: color }}
>
<div className={styles.background} style={{ color }} />
{!sameUserAsLast && (
<UserTooltip user={user}>
<div className={styles.user} style={{ color }}>
@@ -119,11 +122,10 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
<Highlight search={highlightString}>
<div
className={styles.message}
dangerouslySetInnerHTML={{ __html: formattedMessage }}
dangerouslySetInnerHTML={{ __html: linkifyHtml(formattedMessage) }}
/>
</Highlight>
</Tooltip>
{showModeratorMenu && (
<div className={styles.modMenuWrapper}>
<ChatModerationActionMenu
@@ -134,7 +136,6 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
/>
</div>
)}
<div className={styles.background} style={{ color }} />
</div>
</div>
);

View File

@@ -29,10 +29,10 @@ export const ContentHeader: FC<ContentHeaderProps> = ({
<Logo src={logo} />
</div>
<div className={styles.titleSection}>
<div className={cn(styles.title, styles.row, 'header-title')}>{name}</div>
<div className={cn(styles.subtitle, styles.row, 'header-subtitle')}>
<h2 className={cn(styles.title, styles.row, 'header-title')}>{name}</h2>
<h3 className={cn(styles.subtitle, styles.row, 'header-subtitle')}>
<Linkify>{title || summary}</Linkify>
</div>
</h3>
<div className={cn(styles.tagList, styles.row)}>
{tags.length > 0 && tags.map(tag => <span key={tag}>#{tag}&nbsp;</span>)}
</div>

View File

@@ -96,7 +96,12 @@ export const UserDropdown: FC<UserDropdownProps> = ({ username: defaultUsername
Authenticate
</Menu.Item>
{appState.chatAvailable && (
<Menu.Item key="3" icon={<MessageOutlined />} onClick={() => toggleChatVisibility()}>
<Menu.Item
key="3"
icon={<MessageOutlined />}
onClick={() => toggleChatVisibility()}
aria-expanded={chatToggleVisible}
>
{chatToggleVisible ? 'Hide Chat' : 'Show Chat'}
</Menu.Item>
)}

View File

@@ -5,11 +5,13 @@ import Head from 'next/head';
import { FC, useEffect, useRef } from 'react';
import { Layout } from 'antd';
import dynamic from 'next/dynamic';
import Script from 'next/script';
import {
ClientConfigStore,
isChatAvailableSelector,
clientConfigStateAtom,
fatalErrorStateAtom,
appStateAtom,
} from '../../stores/ClientConfigStore';
import { Content } from '../../ui/Content/Content';
import { Header } from '../../ui/Header/Header';
@@ -21,6 +23,7 @@ import { ServerRenderedHydration } from '../../ServerRendered/ServerRenderedHydr
import { Theme } from '../../theme/Theme';
import styles from './Main.module.scss';
import { PushNotificationServiceWorker } from '../../workers/PushNotificationServiceWorker/PushNotificationServiceWorker';
import { AppStateOptions } from '../../stores/application-state';
const lockBodyStyle = `
body {
@@ -45,9 +48,11 @@ export const Main: FC = () => {
const { name, title, customStyles } = clientConfig;
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
const fatalError = useRecoilValue<DisplayableError>(fatalErrorStateAtom);
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
const layoutRef = useRef<HTMLDivElement>(null);
const { chatDisabled } = clientConfig;
const { videoAvailable } = appState;
useEffect(() => {
setupNoLinkReferrer(layoutRef.current);
@@ -133,8 +138,15 @@ export const Main: FC = () => {
<PushNotificationServiceWorker />
<TitleNotifier name={name} />
<Theme />
<Script strategy="afterInteractive" src="/customjavascript" />
<Layout ref={layoutRef} className={styles.layout}>
<Header name={title || name} chatAvailable={isChatAvailable} chatDisabled={chatDisabled} />
<Header
name={title || name}
chatAvailable={isChatAvailable}
chatDisabled={chatDisabled}
online={videoAvailable}
/>
<Content />
{fatalError && (
<FatalErrorStateModal title={fatalError.title} message={fatalError.message} />

View File

@@ -77,6 +77,7 @@ export const NameChangeModal: FC = () => {
value={newName}
onChange={e => setNewName(e.target.value)}
placeholder="Your chat display name"
aria-label="Your chat display name"
maxLength={30}
showCount
defaultValue={displayName}
@@ -90,7 +91,7 @@ export const NameChangeModal: FC = () => {
>
{colorOptions.map(e => (
<Option key={e.toString()} title={e}>
<UserColor color={e} />
<UserColor color={e} aria-label={e.toString()} />
</Option>
))}
</Select>

View File

@@ -35,6 +35,8 @@ const ACCESS_TOKEN_KEY = 'accessToken';
let serverStatusRefreshPoll: ReturnType<typeof setInterval>;
let hasBeenModeratorNotified = false;
const serverConnectivityError = `Cannot connect to the Owncast service. Please check your internet connection or if needed, double check this Owncast server is running.`;
// Server status is what gets updated such as viewer count, durations,
// stream title, online/offline state, etc.
export const serverStatusState = atom<ServerStatus>({
@@ -200,10 +202,7 @@ export const ClientConfigStore: FC = () => {
setGlobalFatalErrorMessage(null);
setHasLoadedConfig(true);
} catch (error) {
setGlobalFatalError(
'Unable to reach Owncast server',
`Owncast cannot launch. Please make sure the Owncast server is running.`,
);
setGlobalFatalError('Unable to reach Owncast server', serverConnectivityError);
console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
}
};
@@ -221,10 +220,7 @@ export const ClientConfigStore: FC = () => {
setGlobalFatalErrorMessage(null);
} catch (error) {
sendEvent(AppStateEvent.Fail);
setGlobalFatalError(
'Unable to reach Owncast server',
`Owncast cannot launch. Please make sure the Owncast server is running.`,
);
setGlobalFatalError('Unable to reach Owncast server', serverConnectivityError);
console.error(`serverStatusState -> getStatus() ERROR: \n${error}`);
}
};
@@ -325,7 +321,13 @@ export const ClientConfigStore: FC = () => {
const startChat = async () => {
try {
const { socketHostOverride } = clientConfig;
const host = socketHostOverride || window.location.toString();
// Get a copy of the browser location without #fragments.
const l = window.location;
l.hash = '';
const location = l.toString().replaceAll('#', '');
const host = socketHostOverride || location;
ws = new WebsocketService(accessToken, '/ws', host);
ws.handleMessage = handleMessage;
setWebsocketService(ws);

View File

@@ -20,14 +20,11 @@
}
.mainSection::-webkit-scrollbar {
width: 5px;
height: auto;
background-color: var(--theme-color-components-scrollbar-background);
display: none;
}
.mainSection::-webkit-scrollbar-thumb {
background: var(--theme-color-components-scrollbar-thumb);
border-radius: 1px;
display: none;
}
.topSection {

View File

@@ -115,7 +115,7 @@ const DesktopContent = ({
return (
<>
<div className={styles.lowerHalf}>
<div className={styles.lowerHalf} id="skip-to-content">
<ContentHeader
name={name}
title={streamTitle}
@@ -233,7 +233,7 @@ export const Content: FC = () => {
const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector);
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
const currentUser = useRecoilValue(currentUserAtom);
const serverStatus = useRecoilValue<ServerStatus>(serverStatusState);
const [isMobile, setIsMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
const online = useRecoilValue<boolean>(isOnlineSelector);
@@ -259,6 +259,7 @@ export const Content: FC = () => {
const { account: fediverseAccount, enabled: fediverseEnabled } = federation;
const { browser: browserNotifications } = notifications;
const { enabled: browserNotificationsEnabled } = browserNotifications;
const { online: isStreamLive } = serverStatus;
const [externalActionToDisplay, setExternalActionToDisplay] = useState<ExternalAction>(null);
const [supportsBrowserNotifications, setSupportsBrowserNotifications] = useState(false);
@@ -334,9 +335,16 @@ export const Content: FC = () => {
<div className={styles.mainSection}>
<div className={styles.topSection}>
{appState.appLoading && <Skeleton loading active paragraph={{ rows: 7 }} />}
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
{online && (
<OwncastPlayer
source="/hls/stream.m3u8"
online={online}
title={streamTitle || name}
/>
)}
{!online && !appState.appLoading && (
<OfflineBanner
showsHeader={false}
streamName={name}
customText={offlineMessage}
notificationsEnabled={browserNotificationsEnabled}
@@ -346,7 +354,7 @@ export const Content: FC = () => {
onFollowClick={() => setShowFollowModal(true)}
/>
)}
{online && (
{isStreamLive && (
<Statusbar
online={online}
lastConnectTime={lastConnectTime}

View File

@@ -8,12 +8,10 @@
justify-content: space-between;
flex-direction: row;
background-color: var(--theme-color-background-header);
width: 100%;
min-height: 30px;
color: var(--theme-color-components-text-on-dark);
font-family: var(--theme-text-body-font-family);
padding: 0 0.6rem;
padding: 0.6rem;
font-size: 0.8rem;
font-weight: 600;
border-top: 1px solid rgba(214, 211, 211, 0.5);

View File

@@ -6,7 +6,7 @@ export type FooterProps = {
};
export const Footer: FC<FooterProps> = ({ version }) => (
<footer className={styles.footer}>
<footer className={styles.footer} id="footer">
<span>
Powered by <a href="https://owncast.online">{version}</a>
</span>

View File

@@ -6,7 +6,7 @@
align-items: center;
justify-content: space-between;
z-index: 20;
padding: 1rem 0.7rem;
padding: 0.7rem;
box-shadow: 0px 1px 3px 1px rgb(0 0 0 / 10%);
background-color: var(--theme-color-background-header);
@@ -42,3 +42,18 @@
overflow: hidden;
line-height: 1.4;
}
.skipLink {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skipLink:focus {
position: static;
width: auto;
height: auto;
}

View File

@@ -2,6 +2,7 @@ import { Tag, Tooltip } from 'antd';
import { FC } from 'react';
import cn from 'classnames';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { OwncastLogo } from '../../common/OwncastLogo/OwncastLogo';
import styles from './Header.module.scss';
@@ -18,19 +19,32 @@ export type HeaderComponentProps = {
name: string;
chatAvailable: boolean;
chatDisabled: boolean;
online: boolean;
};
export const Header: FC<HeaderComponentProps> = ({
name = 'Your stream title',
chatAvailable,
chatDisabled,
online,
}) => (
<header className={cn([`${styles.header}`], 'global-header')}>
{online && (
<Link href="#player" className={styles.skipLink}>
Skip to player
</Link>
)}
<Link href="#skip-to-content" className={styles.skipLink}>
Skip to page content
</Link>
<Link href="#footer" className={styles.skipLink}>
Skip to footer
</Link>
<div className={styles.logo}>
<div id="header-logo" className={styles.logoImage}>
<OwncastLogo variant="contrast" />
</div>
<h1 className={styles.title} id="global-header-text" title={name}>
<h1 className={styles.title} id="global-header-text">
{name}
</h1>
</div>

View File

@@ -9,15 +9,15 @@
flex-direction: column;
color: var(--theme-color-components-text-on-light);
background-color: var(--theme-color-background-main);
margin: 1rem auto;
margin: 3rem auto;
border-radius: var(--theme-rounded-corners);
padding: 1rem;
padding: 2.5em;
font-size: 1.2rem;
border: 1px solid lightgray;
}
.bodyText {
line-height: 1.5rem;
line-height: 2rem;
}
.separator {

View File

@@ -17,6 +17,7 @@ export type OfflineBannerProps = {
lastLive?: Date;
notificationsEnabled: boolean;
fediverseAccount?: string;
showsHeader?: boolean;
onNotifyClick?: () => void;
onFollowClick?: () => void;
};
@@ -27,6 +28,7 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
lastLive,
notificationsEnabled,
fediverseAccount,
showsHeader = true,
onNotifyClick,
onFollowClick,
}) => {
@@ -74,8 +76,12 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
return (
<div id="offline-banner" className={styles.outerContainer}>
<div className={styles.innerContainer}>
<div className={styles.header}>{streamName}</div>
<Divider className={styles.separator} />
{showsHeader && (
<>
<div className={styles.header}>{streamName}</div>
<Divider className={styles.separator} />
</>
)}
<div className={styles.bodyText}>{text}</div>
{lastLive && (
<div className={styles.lastLiveDate}>

View File

@@ -7,7 +7,6 @@ export type SocialLinksProps = {
links: SocialLink[];
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const SocialLinks: FC<SocialLinksProps> = ({ links }) => (
<div className={styles.links}>
{links.map(link => (
@@ -22,7 +21,6 @@ export const SocialLinks: FC<SocialLinksProps> = ({ links }) => (
<Image
src={link.icon || '/img/platformlogos/default.svg'}
alt={link.platform}
title={link.platform}
className={styles.link}
width="30"
height="30"

View File

@@ -6,8 +6,8 @@
height: 2rem;
width: 100%;
padding: var(--content-padding);
color: var(--theme-color-components-text-on-light);
background-color: var(--component-background);
color: var(--theme-color-components-video-status-bar-foreground);
background-color: var(--theme-color-components-video-status-bar-background);
font-family: var(--theme-text-display-font-family);
font-weight: 600;
font-weight: 400;
}

View File

@@ -66,7 +66,7 @@ export const Statusbar: FC<StatusbarProps> = ({
}
return (
<div className={styles.statusbar}>
<div className={styles.statusbar} role="status">
<div>{onlineMessage}</div>
<div>{rightSideMessage}</div>
</div>

View File

@@ -9,13 +9,33 @@
height: 75px;
width: 250px;
font-size: 0.8rem;
overflow: hidden;
@include screen(mobile){
color: var(--theme-color-components-text-on-light);
.name {
font-weight: 600;
font-size: 1rem;
color: var(--theme-color-components-text-on-light);
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
width: calc(85%);
white-space: nowrap;
}
.account {
color: var(--theme-color-components-text-on-light);
word-break: break-all;
line-height: 0.9rem;
}
@include screen(mobile) {
margin: auto;
}
&:hover {
border-color: var(--theme-text-link);
border-color: var(--theme-color-action);
}
.avatar {
@@ -26,11 +46,6 @@
border-style: solid;
}
.account {
color: var(--theme-text-secondary);
text-overflow: ellipsis;
}
.placeholder {
width: 100%;
height: 100%;

View File

@@ -18,7 +18,7 @@ export const SingleFollower: FC<SingleFollowerProps> = ({ follower }) => (
</Avatar>
</Col>
<Col>
<Row>{follower.name}</Row>
<Row className={styles.name}>{follower.name}</Row>
<Row className={styles.account}>{follower.username}</Row>
</Col>
</Row>

View File

@@ -1,16 +1,16 @@
@import '../../../styles/mixins.scss';
.container {
display: grid;
width: 100%;
justify-items: center;
max-height: 75vh;
aspect-ratio: 16 / 9;
.player,
.poster {
// position: static;
// height: auto !important;
width: 100%;
grid-column: 1;
grid-row: 1;
aspect-ratio: 16 / 9;
max-height: 75vh;
}
}

View File

@@ -34,4 +34,5 @@ export const LiveDemo = Template.bind({});
LiveDemo.args = {
online: true,
source: 'https://watch.owncast.online/hls/stream.m3u8',
title: 'Stream title',
};

View File

@@ -10,7 +10,6 @@ import { isVideoPlayingAtom, clockSkewAtom } from '../../stores/ClientConfigStor
import PlaybackMetrics from '../metrics/playback';
import createVideoSettingsMenuButton from '../settings-menu';
import LatencyCompensator from '../latencyCompensator';
import styles from './OwncastPlayer.module.scss';
const VIDEO_CONFIG_URL = '/api/video/variants';
@@ -26,6 +25,7 @@ export type OwncastPlayerProps = {
source: string;
online: boolean;
initiallyMuted?: boolean;
title: string;
};
async function getVideoSettings() {
@@ -44,6 +44,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
source,
online,
initiallyMuted = false,
title,
}) => {
const playerRef = React.useRef(null);
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
@@ -85,13 +86,13 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
}
};
const setLatencyCompensatorItemTitle = title => {
const setLatencyCompensatorItemTitle = t => {
const item = document.querySelector('.latency-toggle-item > .vjs-menu-item-text');
if (!item) {
return;
}
item.innerHTML = title;
item.innerHTML = t;
};
const startLatencyCompensator = () => {
@@ -218,6 +219,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
controls: true,
responsive: true,
fluid: false,
fill: true,
playsinline: true,
liveui: true,
preload: 'auto',
@@ -306,10 +308,10 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
);
return (
<div className={styles.container}>
<div className={styles.container} id="player">
{online && (
<div className={styles.player}>
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} aria-label={title} />
</div>
)}
<div className={styles.poster}>

View File

@@ -1,9 +1,5 @@
@import '../../../styles/mixins.scss';
.player {
height: auto !important;
width: 100%;
video {
position: static !important;
}
}

View File

@@ -33,9 +33,9 @@ export const VideoPoster: FC<VideoPosterProps> = ({ online, initialSrc, src: bas
<CrossfadeImage
src={src}
duration={duration}
objectFit="cover"
objectFit="contain"
height="auto"
width="100%"
height="100%"
/>
)}
</div>

View File

@@ -93,6 +93,7 @@ export function createVideoSettingsMenuButton(player, videojs, qualities, latenc
}
const menuButton = new MenuButton();
menuButton.el().setAttribute('aria-label', 'Settings');
// If none of the settings in this menu are applicable then don't show it.
const tech = player.tech({ IWillNotUseThisInPlugins: true });

View File

@@ -43,6 +43,10 @@ module.exports = withBundleAnalyzer(
source: '/thumbnail.jpg',
destination: 'http://localhost:8080/thumbnail.jpg', // Proxy to Backend to work around CORS.
},
{
source: '/customjavascript',
destination: 'http://localhost:8080/customjavascript', // Proxy to Backend to work around CORS.
},
];
},
pageExtensions: ['tsx'],

3860
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,23 +14,26 @@
"dependencies": {
"@ant-design/icons": "4.8.0",
"@codemirror/lang-css": "6.0.1",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/lang-markdown": "6.0.5",
"@codemirror/language-data": "6.1.0",
"@fontsource/open-sans": "4.5.13",
"@fontsource/open-sans": "4.5.14",
"@fontsource/poppins": "4.5.10",
"@uiw/codemirror-theme-bbedit": "4.19.6",
"@uiw/react-codemirror": "4.19.6",
"@xstate/react": "3.0.1",
"@next/bundle-analyzer": "^13.1.1",
"@uiw/codemirror-theme-bbedit": "4.19.7",
"@uiw/react-codemirror": "4.19.7",
"@xstate/react": "3.0.2",
"antd": "4.24.3",
"autoprefixer": "10.4.13",
"chart.js": "4.1.2",
"chartkick": "4.2.0",
"chart.js": "4.2.0",
"chartkick": "5.0.1",
"classnames": "2.3.2",
"date-fns": "2.29.3",
"entities": "^4.4.0",
"install": "^0.13.0",
"linkify-html": "^4.1.0",
"linkifyjs": "^4.1.0",
"lodash": "4.17.21",
"next": "13.1.2",
"next": "13.1.5",
"next-with-less": "2.0.5",
"picmo": "5.7.2",
"postcss-flexbugs-fixes": "5.0.2",
@@ -42,21 +45,17 @@
"react-highlighter-ts": "18.0.1",
"react-hotkeys-hook": "4.3.2",
"react-linkify": "1.0.0-alpha",
"react-markdown": "8.0.4",
"react-markdown": "8.0.5",
"react-use": "^17.4.0",
"react-virtuoso": "4.0.3",
"react-virtuoso": "4.0.5",
"recoil": "0.7.6",
"sharp": "0.31.3",
"slate": "0.88.1",
"slate-react": "0.88.0",
"storybook-addon-designs": "6.3.1",
"storybook-addon-fetch-mock": "1.0.1",
"style-dictionary": "3.7.2",
"ua-parser-js": "1.0.32",
"slate-react": "0.88.2",
"ua-parser-js": "1.0.33",
"video.js": "7.20.3",
"xstate": "4.35.2",
"yaml": "2.2.1",
"@next/bundle-analyzer": "^13.1.1"
"yaml": "2.2.1"
},
"devDependencies": {
"@babel/core": "7.20.12",
@@ -81,35 +80,40 @@
"@types/markdown-it": "12.2.3",
"@types/node": "18.11.18",
"@types/prop-types": "15.7.5",
"@types/react": "18.0.26",
"@types/react": "18.0.27",
"@types/react-linkify": "1.0.1",
"@types/ua-parser-js": "0.7.36",
"@types/video.js": "^7.3.50",
"@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.1",
"@typescript-eslint/eslint-plugin": "5.49.0",
"@typescript-eslint/parser": "5.49.0",
"babel-loader": "9.1.2",
"chromatic": "6.14.0",
"chromatic": "6.15.0",
"css-loader": "6.7.3",
"cypress": "^12.0.0",
"eslint": "8.31.0",
"eslint": "8.32.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-next": "13.1.2",
"eslint-config-next": "13.1.5",
"eslint-config-prettier": "8.6.0",
"eslint-plugin-import": "2.27.4",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.0",
"eslint-plugin-react": "7.32.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-storybook": "0.6.10",
"handlebars": "^4.7.7",
"html-webpack-plugin": "5.5.0",
"install": "^0.13.0",
"less": "4.1.3",
"less-loader": "11.1.0",
"npm": "^9.4.0",
"prettier": "2.8.3",
"sass": "1.57.1",
"sass-loader": "13.2.0",
"sb": "6.5.15",
"storybook-dark-mode": "2.0.5",
"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"
}

View File

@@ -4,7 +4,6 @@ import Link from 'next/link';
import Discord from '../../components/admin/notification/discord';
import Browser from '../../components/admin/notification/browser';
import Twitter from '../../components/admin/notification/twitter';
import Federation from '../../components/admin/notification/federation';
import {
TextFieldWithSubmit,
@@ -99,13 +98,6 @@ export default function ConfigNotify() {
>
<Browser />
</Col>
<Col
span={10}
className={`form-module ${enabled ? '' : 'disabled'}`}
style={{ margin: '5px', display: 'flex', flexDirection: 'column' }}
>
<Twitter />
</Col>
<Col
span={10}

View File

@@ -5,6 +5,7 @@ import GeneralConfig from '../../../../components/admin/config/general/GeneralCo
import AppearanceConfig from '../../../../components/admin/config/general/AppearanceConfig';
import { AdminLayout } from '../../../../components/layouts/AdminLayout';
import { EditCustomJavascript } from '../../../../components/admin/EditCustomJavascript';
export default function PublicFacingDetails() {
return (
@@ -23,6 +24,11 @@ export default function PublicFacingDetails() {
key: '2',
children: <AppearanceConfig />,
},
{
label: `Custom Scripting`,
key: '3',
children: <EditCustomJavascript />,
},
]}
/>
</div>

View File

@@ -6,16 +6,20 @@ import {
currentUserAtom,
visibleChatMessagesSelector,
clientConfigStateAtom,
appStateAtom,
} from '../../../../components/stores/ClientConfigStore';
import Header from '../../../../components/ui/Header/Header';
import { ClientConfig } from '../../../../interfaces/client-config.model';
import { AppStateOptions } from '../../../../components/stores/application-state';
export default function ReadWriteChatEmbed() {
const currentUser = useRecoilValue(currentUserAtom);
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
const { name, chatDisabled } = clientConfig;
const { videoAvailable } = appState;
if (!currentUser) {
return null;
@@ -26,7 +30,7 @@ export default function ReadWriteChatEmbed() {
return (
<div>
<ClientConfigStore />
<Header name={name} chatAvailable chatDisabled={chatDisabled} />
<Header name={name} chatAvailable chatDisabled={chatDisabled} online={videoAvailable} />
<ChatContainer
messages={messages}
usernameToHighlight={displayName}

View File

@@ -20,7 +20,7 @@ export default function VideoEmbed() {
const { name } = clientConfig;
const { offlineMessage } = clientConfig;
const { viewerCount, lastConnectTime, lastDisconnectTime } = status;
const { viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } = status;
const online = useRecoilValue<boolean>(isOnlineSelector);
const router = useRouter();
@@ -48,6 +48,7 @@ export default function VideoEmbed() {
source="/hls/stream.m3u8"
online={online}
initiallyMuted={initiallyMuted}
title={streamTitle || name}
/>
)}
{!online && (

View File

@@ -147,7 +147,7 @@ theme:
value: 'var(--theme-color-palette-10)'
comment: '{theme.color.palette.10.comment}'
border:
value: 'var(--theme-color-palette-4)'
value: 'var(--theme-color-action)'
comment: '{theme.color.palette.4.comment}'
border-disabled:
value: 'var(--theme-color-action-disabled)'
@@ -174,25 +174,17 @@ theme:
chat:
background:
value: 'var(--theme-color-palette-1)'
comment: '{theme.color.palette.0.comment}'
text:
value: 'var(--theme-color-palette-15)'
comment: '{theme.color.palette.15.comment}'
text:
value: 'var(--theme-color-palette-2)'
comment: '{theme.color.palette.2.comment}'
content:
background:
value: 'var(--theme-color-palette-15)'
comment: '{theme.color.palette.15.comment}'
scrollbar:
background:
value: 'var(--theme-color-palette-15)'
comment: '{theme.color.palette.15.comment}'
thumb:
value: 'var(--theme-color-palette-6)'
comment: '{theme.color.palette.6.comment}'
modal:
header:
background:
@@ -245,3 +237,10 @@ theme:
live-indicator:
value: 'var(--theme-color-palette-7)'
comment: 'The Live dot indicator in the control bar of the video player'
status-bar:
background:
value: 'var(--theme-color-palette-2)'
comment: 'The background color of the video status bar'
foreground:
value: 'var(--theme-color-palette-4)'
comment: 'The foreground color of the video status bar'

View File

@@ -12,23 +12,24 @@ color:
# If you add more colors here make sure to add them to
# GenerateRandomDisplayColor in the Go codebase so it knows the max
# number of colors to use.
user:
0:
value: 'rgb(244, 11, 11)'
value: '#ff717b'
1:
value: 'rgb(244, 128, 11)'
value: '#F4E413'
2:
value: 'rgb(162, 162, 1)'
value: '#b99c45'
3:
value: 'rgb(88, 244, 11)'
value: '#58f40b'
4:
value: 'rgb(11, 244, 244)'
value: '#0bf4f4'
5:
value: 'rgb(11, 166, 244)'
value: '#0ba6f4'
6:
value: 'rgb(102, 102, 255)'
value: '#9a92ff'
7:
value: 'rgb(244, 11, 244)'
value: '#ff53ff'
palette:
0:
@@ -62,7 +63,7 @@ color:
value: '#39373d'
comment: 'Neutral dark'
10:
value: '#707283'
value: '#5d5f72'
comment: 'Neutral gray light'
11:
value: '#2386e2'

View File

@@ -25,7 +25,7 @@ BUTTONS
.ant-btn-default {
color: currentColor;
border-width: 2px;
border-color: transparent;
border-color: var(--theme-color-components-primary-button-border);
border-radius: var(--theme-rounded-corners);
background-color: rgba(0, 0, 0, 0.1);
&:hover,
@@ -43,6 +43,7 @@ BUTTONS
border-width: 2px;
border-radius: var(--theme-rounded-corners);
color: var(--theme-color-components-primary-button-text);
border-color: var(--theme-color-components-primary-button-border);
&:hover,
&:focus {
@@ -59,7 +60,6 @@ BUTTONS
}
background-color: var(--theme-color-components-primary-button-background);
color: var(--theme-color-components-primary-button-text);
border-color: var(--theme-color-components-primary-button-background);
&:hover {
background-color: var(--theme-color-action-hover);
color: var(--theme-color-components-primary-button-text);
@@ -158,7 +158,7 @@ DROPDOWN
padding: var(--content-padding);
background-color: transparent;
border-radius: var(--theme-rounded-corners) var(--theme-rounded-corners) 0 0;
font-weight: bold;
font-weight: 600;
& + .ant-tabs-tab {
margin-left: var(--module-spacing);
}
@@ -213,3 +213,7 @@ th {
font-family: var(--theme-text-display-font-family);
font-weight: 700 !important;
}
.ant-popover {
z-index: 800; // Lower the z-index so it renders under modals.
}

View File

@@ -9,7 +9,7 @@
:root {
--content-padding: 12px;
--module-spacing: 12px; // margin size between lines of stuff, if needed
--header-height: 5.375rem; // needed for making main content scrollable;
--header-height: 4.3rem; // needed for making main content scrollable;
--footer-height: 2.5rem; // needed for making main content scrollable;
--content-height: calc(100vh - var(--header-height));
--replacement-bar-height: 46px; // needed for making main content scrollable on mobile;

View File

@@ -1,6 +1,6 @@
// Do not edit directly
// Generated on Wed, 21 Dec 2022 07:38:01 GMT
// Generated on Sun, 29 Jan 2023 01:28:51 GMT
//
// How to edit these values:
// Edit the corresponding token file under the style-definitions directory
@@ -75,7 +75,7 @@
@theme-color-components-primary-button-background-disabled: var(--theme-color-action-disabled); // Disabled background
@theme-color-components-primary-button-text: var(--theme-color-palette-4); // Light secondary
@theme-color-components-primary-button-text-disabled: var(--theme-color-palette-10); // Neutral gray light
@theme-color-components-primary-button-border: var(--theme-color-palette-4); // Light secondary
@theme-color-components-primary-button-border: var(--theme-color-action); // Light secondary
@theme-color-components-primary-button-border-disabled: var(--theme-color-action-disabled); // Disabled background
@theme-color-components-secondary-button-background: var(--theme-color-palette-4); // Light secondary
@theme-color-components-secondary-button-background-disabled: transparent;
@@ -83,11 +83,9 @@
@theme-color-components-secondary-button-text-disabled: var(--theme-color-action-disabled); // Disabled background
@theme-color-components-secondary-button-border: var(--theme-color-action); // Text link/secondary light text
@theme-color-components-secondary-button-border-disabled: var(--theme-color-action-disabled); // Disabled background
@theme-color-components-chat-background: var(--theme-color-palette-15); // Lighter background
@theme-color-components-chat-text: var(--theme-color-palette-2); // Dark alternate
@theme-color-components-chat-background: var(--theme-color-palette-1); // Dark primary
@theme-color-components-chat-text: var(--theme-color-palette-15); // Lighter background
@theme-color-components-content-background: var(--theme-color-palette-15); // Lighter background
@theme-color-components-scrollbar-background: var(--theme-color-palette-15); // Lighter background
@theme-color-components-scrollbar-thumb: var(--theme-color-palette-6); // Text link/secondary light text
@theme-color-components-modal-header-background: var(--theme-color-palette-1); // Dark secondary
@theme-color-components-modal-header-text: var(--theme-color-palette-3); // Light primary
@theme-color-components-modal-content-background: var(--theme-color-palette-3); // Light primary
@@ -103,17 +101,19 @@
@theme-color-components-form-field-border: var(--theme-color-palette-0); // Dark primary
@theme-color-components-video-background: var(--theme-color-palette-2); // Dark alternate
@theme-color-components-video-live-indicator: var(--theme-color-palette-7); // The Live dot indicator in the control bar of the video player
@theme-color-components-video-status-bar-background: var(--theme-color-palette-2); // The background color of the video status bar
@theme-color-components-video-status-bar-foreground: var(--theme-color-palette-4); // The foreground color of the video status bar
@owncast-purple-25: rgba(120, 113, 255, 0.25);
@color-unknown: #7a5cf3;
@color-unknown-2: #fffffe;
@color-owncast-user-0: #f40b0b;
@color-owncast-user-1: #f4800b;
@color-owncast-user-2: #a2a201;
@color-owncast-user-0: #ff717b;
@color-owncast-user-1: #f4e413;
@color-owncast-user-2: #b99c45;
@color-owncast-user-3: #58f40b;
@color-owncast-user-4: #0bf4f4;
@color-owncast-user-5: #0ba6f4;
@color-owncast-user-6: #6666ff;
@color-owncast-user-7: #f40bf4;
@color-owncast-user-6: #9a92ff;
@color-owncast-user-7: #ff53ff;
@color-owncast-palette-0: #12161d; // Dark primary
@color-owncast-palette-1: #2d3748; // Dark secondary
@color-owncast-palette-2: #000000; // Dark alternate
@@ -124,7 +124,7 @@
@color-owncast-palette-7: #5d38f3; // Text link hover
@color-owncast-palette-8: #b6b3c6; // Disabled background
@color-owncast-palette-9: #39373d; // Neutral dark
@color-owncast-palette-10: #707283; // Neutral gray light
@color-owncast-palette-10: #5d5f72; // Neutral gray light
@color-owncast-palette-11: #2386e2; // Fun color 1
@color-owncast-palette-12: #da9eff; // Fun color 2
@color-owncast-palette-13: #42bea6; // Fun color 3

View File

@@ -1,6 +1,6 @@
/**
* Do not edit directly
* Generated on Wed, 21 Dec 2022 07:38:01 GMT
* Generated on Sun, 29 Jan 2023 01:28:51 GMT
*
* How to edit these values:
* Edit the corresponding token file under the style-definitions directory
@@ -35,12 +35,8 @@
--theme-rounded-corners: 9px; /* How much corners are rounded in places in the UI. */
--theme-unknown-1: green; /* This should never be used and it means something is wrong. */
--theme-unknown-2: red; /* This should never be used and it means something is wrong. */
--theme-text-body-font-family: var(
--font-owncast-body
); /* The font family used for the body text. */
--theme-text-display-font-family: var(
--font-owncast-display
); /* The font family used for the display/header text. */
--theme-text-body-font-family: var(--font-owncast-body); /* The font family used for the body text. */
--theme-text-display-font-family: var(--font-owncast-display); /* The font family used for the display/header text. */
--theme-color-users-0: var(--color-owncast-user-0);
--theme-color-users-1: var(--color-owncast-user-1);
--theme-color-users-2: var(--color-owncast-user-2);
@@ -77,85 +73,49 @@
--theme-color-warning: var(--theme-color-palette-warning); /* Warning */
--theme-color-components-text-on-light: var(--theme-color-palette-0); /* Dark primary */
--theme-color-components-text-on-dark: var(--theme-color-palette-3); /* Light primary */
--theme-color-components-primary-button-background: var(
--theme-color-action
); /* Text link/secondary light text */
--theme-color-components-primary-button-background-disabled: var(
--theme-color-action-disabled
); /* Disabled background */
--theme-color-components-primary-button-background: var(--theme-color-action); /* Text link/secondary light text */
--theme-color-components-primary-button-background-disabled: var(--theme-color-action-disabled); /* Disabled background */
--theme-color-components-primary-button-text: var(--theme-color-palette-4); /* Light secondary */
--theme-color-components-primary-button-text-disabled: var(
--theme-color-palette-10
); /* Neutral gray light */
--theme-color-components-primary-button-border: var(
--theme-color-palette-4
); /* Light secondary */
--theme-color-components-primary-button-border-disabled: var(
--theme-color-action-disabled
); /* Disabled background */
--theme-color-components-secondary-button-background: var(
--theme-color-palette-4
); /* Light secondary */
--theme-color-components-primary-button-text-disabled: var(--theme-color-palette-10); /* Neutral gray light */
--theme-color-components-primary-button-border: var(--theme-color-action); /* Light secondary */
--theme-color-components-primary-button-border-disabled: var(--theme-color-action-disabled); /* Disabled background */
--theme-color-components-secondary-button-background: var(--theme-color-palette-4); /* Light secondary */
--theme-color-components-secondary-button-background-disabled: transparent;
--theme-color-components-secondary-button-text: var(
--theme-color-action-disabled
); /* Disabled background */
--theme-color-components-secondary-button-text-disabled: var(
--theme-color-action-disabled
); /* Disabled background */
--theme-color-components-secondary-button-border: var(
--theme-color-action
); /* Text link/secondary light text */
--theme-color-components-secondary-button-border-disabled: var(
--theme-color-action-disabled
); /* Disabled background */
--theme-color-components-chat-background: var(--theme-color-palette-15); /* Lighter background */
--theme-color-components-chat-text: var(--theme-color-palette-2); /* Dark alternate */
--theme-color-components-content-background: var(
--theme-color-palette-15
); /* Lighter background */
--theme-color-components-scrollbar-background: var(
--theme-color-palette-15
); /* Lighter background */
--theme-color-components-scrollbar-thumb: var(
--theme-color-palette-6
); /* Text link/secondary light text */
--theme-color-components-modal-header-background: var(
--theme-color-palette-1
); /* Dark secondary */
--theme-color-components-secondary-button-text: var(--theme-color-action-disabled); /* Disabled background */
--theme-color-components-secondary-button-text-disabled: var(--theme-color-action-disabled); /* Disabled background */
--theme-color-components-secondary-button-border: var(--theme-color-action); /* Text link/secondary light text */
--theme-color-components-secondary-button-border-disabled: var(--theme-color-action-disabled); /* Disabled background */
--theme-color-components-chat-background: var(--theme-color-palette-1); /* Dark primary */
--theme-color-components-chat-text: var(--theme-color-palette-15); /* Lighter background */
--theme-color-components-content-background: var(--theme-color-palette-15); /* Lighter background */
--theme-color-components-modal-header-background: var(--theme-color-palette-1); /* Dark secondary */
--theme-color-components-modal-header-text: var(--theme-color-palette-3); /* Light primary */
--theme-color-components-modal-content-background: var(
--theme-color-palette-3
); /* Light primary */
--theme-color-components-modal-content-background: var(--theme-color-palette-3); /* Light primary */
--theme-color-components-modal-content-text: var(--theme-color-palette-0); /* Dark primary */
--theme-color-components-menu-background: var(--theme-color-palette-3); /* Light primary */
--theme-color-components-menu-item-text: var(--theme-color-palette-0); /* Dark primary */
--theme-color-components-menu-item-bg: transparent;
--theme-color-components-menu-item-hover-bg: rgba(0, 0, 0, 0.05);
--theme-color-components-menu-item-focus-bg: rgba(0, 0, 0, 0.1);
--theme-color-components-form-field-background: var(
--theme-color-palette-4
); /* Light secondary */
--theme-color-components-form-field-placeholder: var(
--theme-color-action-disabled
); /* Disabled background */
--theme-color-components-form-field-background: var(--theme-color-palette-4); /* Light secondary */
--theme-color-components-form-field-placeholder: var(--theme-color-action-disabled); /* Disabled background */
--theme-color-components-form-field-text: var(--theme-color-palette-0); /* Dark primary */
--theme-color-components-form-field-border: var(--theme-color-palette-0); /* Dark primary */
--theme-color-components-video-background: var(--theme-color-palette-2); /* Dark alternate */
--theme-color-components-video-live-indicator: var(
--theme-color-palette-7
); /* The Live dot indicator in the control bar of the video player */
--theme-color-components-video-live-indicator: var(--theme-color-palette-7); /* The Live dot indicator in the control bar of the video player */
--theme-color-components-video-status-bar-background: var(--theme-color-palette-2); /* The background color of the video status bar */
--theme-color-components-video-status-bar-foreground: var(--theme-color-palette-4); /* The foreground color of the video status bar */
--owncast-purple-25: rgba(120, 113, 255, 0.25);
--color-unknown: #7a5cf3;
--color-unknown-2: #fffffe;
--color-owncast-user-0: #f40b0b;
--color-owncast-user-1: #f4800b;
--color-owncast-user-2: #a2a201;
--color-owncast-user-0: #ff717b;
--color-owncast-user-1: #f4e413;
--color-owncast-user-2: #b99c45;
--color-owncast-user-3: #58f40b;
--color-owncast-user-4: #0bf4f4;
--color-owncast-user-5: #0ba6f4;
--color-owncast-user-6: #6666ff;
--color-owncast-user-7: #f40bf4;
--color-owncast-user-6: #9a92ff;
--color-owncast-user-7: #ff53ff;
--color-owncast-palette-0: #12161d; /* Dark primary */
--color-owncast-palette-1: #2d3748; /* Dark secondary */
--color-owncast-palette-2: #000000; /* Dark alternate */
@@ -166,7 +126,7 @@
--color-owncast-palette-7: #5d38f3; /* Text link hover */
--color-owncast-palette-8: #b6b3c6; /* Disabled background */
--color-owncast-palette-9: #39373d; /* Neutral dark */
--color-owncast-palette-10: #707283; /* Neutral gray light */
--color-owncast-palette-10: #5d5f72; /* Neutral gray light */
--color-owncast-palette-11: #2386e2; /* Fun color 1 */
--color-owncast-palette-12: #da9eff; /* Fun color 2 */
--color-owncast-palette-13: #42bea6; /* Fun color 3 */
@@ -174,10 +134,6 @@
--color-owncast-palette-15: #eff1f4; /* Lighter background */
--color-owncast-palette-error: #ff4b39; /* Error */
--color-owncast-palette-warning: #ffc655; /* Warning */
--font-owncast-body: 'Open Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--font-owncast-display: 'Poppins', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--font-owncast-body: 'Open Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-owncast-display: 'Poppins', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}

View File

@@ -29,6 +29,7 @@ export interface ConfigDirectoryFields {
export interface ConfigInstanceDetailsFields {
customStyles: string;
customJavascript: string;
extraPageContent: string;
logo: string;
name: string;
@@ -115,20 +116,9 @@ export interface DiscordNotification {
goLiveMessage: string;
}
export interface TwitterNotification {
enabled: boolean;
apiKey: string;
apiSecret: string;
accessToken: string;
accessTokenSecret: string;
bearerToken: string;
goLiveMessage: string;
}
export interface NotificationsConfig {
browser: BrowserNotification;
discord: DiscordNotification;
twitter: TwitterNotification;
}
export interface Health {

View File

@@ -11,6 +11,7 @@ export const RESET_TIMEOUT = 3000;
// CONFIG API ENDPOINTS
export const API_CUSTOM_CONTENT = '/pagecontent';
export const API_CUSTOM_CSS_STYLES = '/customstyles';
export const API_CUSTOM_JAVASCRIPT = '/customjavascript';
export const API_FFMPEG = '/ffmpegpath';
export const API_INSTANCE_URL = '/serverurl';
export const API_LOGO = '/logo';
@@ -557,48 +558,3 @@ export const BROWSER_PUSH_CONFIG_FIELDS = {
placeholder: `I've gone live! Come watch!`,
},
};
export const TWITTER_CONFIG_FIELDS = {
apiKey: {
fieldName: 'apiKey',
label: 'API Key',
maxLength: 200,
tip: '',
placeholder: `gaUQhRC2lqfrEFfElBXJgOctU`,
},
apiSecret: {
fieldName: 'apiSecret',
label: 'API Secret',
maxLength: 200,
tip: '',
placeholder: `IIz4jFZMWbUKdFOEGUprFjRwIslG56d1SPQlolJYjXwJ2y2qKS`,
},
accessToken: {
fieldName: 'accessToken',
label: 'Access Token',
maxLength: 200,
tip: '',
placeholder: `952540400-EEiwe9fkuSvWjnNC82YFa9kgpqbyAP3J7FjE2dkka`,
},
accessTokenSecret: {
fieldName: 'accessTokenSecret',
label: 'Access Token Secret',
maxLength: 200,
tip: '',
placeholder: `xO0AZWNGfZxpNsYPg3zNEKhAsPPGvNZFlzQArA2khI9Kg`,
},
bearerToken: {
fieldName: 'bearerToken',
label: 'Bearer Token',
maxLength: 200,
tip: '',
placeholder: `AAAAAAAAAAAAAAFqpXwEAAnnepHkjA8XD5ftx5jUadYIRtPtaq7AAAAwpXPpDWKDcdhiWr0tVDjsgW%2B4awGOM9VQ%3XPoMFuWcHsE42TK`,
},
goLiveMessage: {
fieldName: 'goLiveMessage',
label: 'Go Live Text',
maxLength: 200,
tip: 'The text to send when you go live.',
placeholder: `I've gone live! Come watch!`,
},
};

View File

@@ -12,6 +12,7 @@ export const initialServerConfigState: ConfigDetails = {
adminPassword: '',
instanceDetails: {
customStyles: '',
customJavascript: '',
extraPageContent: '',
logo: '',
name: '',
@@ -60,15 +61,6 @@ export const initialServerConfigState: ConfigDetails = {
notifications: {
browser: { enabled: false, goLiveMessage: '' },
discord: { enabled: false, webhook: '', goLiveMessage: '' },
twitter: {
enabled: false,
goLiveMessage: '',
apiKey: '',
apiSecret: '',
accessToken: '',
accessTokenSecret: '',
bearerToken: '',
},
},
externalActions: [],
supportedCodecs: [],