Merge branch 'develop' into fix/ImplementPasswordRules
@@ -4,6 +4,68 @@ import '../styles/theme.less';
|
||||
import './preview.scss';
|
||||
import { themes } from '@storybook/theming';
|
||||
import { DocsContainer } from './storybook-theme';
|
||||
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
|
||||
import _ from 'lodash';
|
||||
|
||||
/**
|
||||
* Takes an entry of a viewport (from Object.entries()) and converts it
|
||||
* into two entries, one for landscape and one for portrait.
|
||||
*
|
||||
* @template {string} Key
|
||||
*
|
||||
* @param {[Key, import('@storybook/addon-viewport/dist/ts3.9/models').Viewport]} entry
|
||||
* @returns {Array<[`${Key}${'Portrait' | 'Landscape'}`, import('@storybook/addon-viewport/dist/ts3.9/models').Viewport]>}
|
||||
*/
|
||||
const convertToLandscapeAndPortraitEntries = ([objectKey, viewport]) => {
|
||||
const pixelStringToNumber = str => parseInt(str.split('px')[0]);
|
||||
const dimensions = [viewport.styles.width, viewport.styles.height].map(pixelStringToNumber);
|
||||
const minDimension = Math.min(...dimensions);
|
||||
const maxDimension = Math.max(...dimensions);
|
||||
|
||||
return [
|
||||
[
|
||||
`${objectKey}Portrait`,
|
||||
{
|
||||
...viewport,
|
||||
name: viewport.name + ' (Portrait)',
|
||||
styles: {
|
||||
...viewport.styles,
|
||||
height: maxDimension + 'px',
|
||||
width: minDimension + 'px',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
`${objectKey}Landscape`,
|
||||
{
|
||||
...viewport,
|
||||
name: viewport.name + ' (Landscape)',
|
||||
styles: {
|
||||
...viewport.styles,
|
||||
height: minDimension + 'px',
|
||||
width: maxDimension + 'px',
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes an object and a function f and returns a new object.
|
||||
* f takes the original object's entries (key-value-pairs
|
||||
* from Object.entries) and returns a list of new entries
|
||||
* (also key-value-pairs). These new entries then form the
|
||||
* result.
|
||||
* @template {string | number} OriginalKey
|
||||
* @template {string | number} NewKey
|
||||
* @template OriginalValue
|
||||
* @template OriginalValue
|
||||
*
|
||||
* @param {Record<OriginalKey, OriginalValue>} obj
|
||||
* @param {(entry: [OriginalKey, OriginalValue], index: number, all: Array<[OriginalKey, OriginalValue]>) => Array<[NewKey, NewValue]>} f
|
||||
* @returns {Record<NewKey, NevValue>}
|
||||
*/
|
||||
const flatMapObject = (obj, f) => Object.fromEntries(Object.entries(obj).flatMap(f));
|
||||
|
||||
export const parameters = {
|
||||
fetchMock: {
|
||||
@@ -13,6 +75,8 @@ export const parameters = {
|
||||
docs: {
|
||||
container: DocsContainer,
|
||||
},
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
viewMode: 'docs',
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
@@ -34,4 +98,9 @@ export const parameters = {
|
||||
// Override the default light theme
|
||||
light: { ...themes.normal },
|
||||
},
|
||||
viewport: {
|
||||
// Take a bunch of viewports from the storybook addon and convert them
|
||||
// to portrait + landscape. Keys are appended with 'Landscape' or 'Portrait'.
|
||||
viewports: flatMapObject(INITIAL_VIEWPORTS, convertToLandscapeAndPortraitEntries),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { ColorRow } from './Color';
|
||||
|
||||
<Meta title="owncast/Styles/Colors" parameters={{ chromatic: { disableSnapshot: true } }} />
|
||||
|
||||
# Default theme colors
|
||||
|
||||
These colors are assigned in our [color token](https://github.com/owncast/owncast/tree/develop/web/style-definitions/tokens/color) files
|
||||
and get reflected here as they change. run `npm run build-styles` to regenerate.
|
||||
|
||||
<Story name="Default Theme">
|
||||
|
||||
## Default Theme
|
||||
|
||||
These color names are assigned to specific component variables. They can be overwritten via CSS.
|
||||
|
||||
<ColorRow
|
||||
colors={[
|
||||
'theme-color-palette-0',
|
||||
'theme-color-palette-1',
|
||||
'theme-color-palette-2',
|
||||
'theme-color-palette-3',
|
||||
'theme-color-palette-4',
|
||||
'theme-color-palette-5',
|
||||
'theme-color-palette-6',
|
||||
'theme-color-palette-7',
|
||||
'theme-color-palette-8',
|
||||
'theme-color-palette-9',
|
||||
'theme-color-palette-10',
|
||||
'theme-color-palette-11',
|
||||
'theme-color-palette-12',
|
||||
'theme-color-palette-13',
|
||||
'theme-color-palette-15',
|
||||
'theme-color-palette-error',
|
||||
'theme-color-palette-warning',
|
||||
'theme-color-background-main',
|
||||
'theme-color-background-light',
|
||||
'theme-color-background-header',
|
||||
'theme-color-action',
|
||||
'theme-color-action-hover',
|
||||
'theme-color-action-disabled',
|
||||
]}
|
||||
/>
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="Frontend Components"
|
||||
>
|
||||
## Component Colors
|
||||
|
||||
<ColorRow
|
||||
colors={[
|
||||
'theme-color-components-text-on-light',
|
||||
'theme-color-components-text-on-dark',
|
||||
'theme-color-components-primary-button-background',
|
||||
'theme-color-components-primary-button-background-disabled',
|
||||
'theme-color-components-primary-button-text',
|
||||
'theme-color-components-primary-button-text-disabled',
|
||||
'theme-color-components-primary-button-border',
|
||||
'theme-color-components-secondary-button-background',
|
||||
'theme-color-components-secondary-button-background-disabled',
|
||||
'theme-color-components-secondary-button-text',
|
||||
'theme-color-components-secondary-button-text-disabled',
|
||||
'theme-color-components-secondary-button-border',
|
||||
'theme-color-components-chat-background',
|
||||
'theme-color-components-chat-text',
|
||||
'theme-color-components-modal-header-background',
|
||||
'theme-color-components-modal-header-text',
|
||||
'theme-color-components-modal-content-background',
|
||||
'theme-color-components-content-background',
|
||||
'theme-color-components-modal-content-text',
|
||||
'theme-color-components-menu-background',
|
||||
'theme-color-components-menu-item-text',
|
||||
'theme-color-components-menu-item-bg',
|
||||
'theme-color-components-menu-item-hover-bg',
|
||||
'theme-color-components-menu-item-focus-bg',
|
||||
'theme-color-components-form-field-background',
|
||||
'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.
|
||||
They should not be overwritten, instead the theme variables should be overwritten.
|
||||
|
||||
<ColorRow
|
||||
colors={[
|
||||
'color-owncast-palette-0',
|
||||
'color-owncast-palette-1',
|
||||
'color-owncast-palette-2',
|
||||
'color-owncast-palette-3',
|
||||
'color-owncast-palette-4',
|
||||
'color-owncast-palette-5',
|
||||
'color-owncast-palette-6',
|
||||
'color-owncast-palette-7',
|
||||
'color-owncast-palette-9',
|
||||
'color-owncast-palette-10',
|
||||
'color-owncast-palette-11',
|
||||
'color-owncast-palette-12',
|
||||
'color-owncast-palette-13',
|
||||
'color-owncast-palette-15',
|
||||
]}
|
||||
/>
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="User Chat Colors"
|
||||
>
|
||||
## User Colors
|
||||
|
||||
<ColorRow
|
||||
colors={[
|
||||
'theme-color-users-0',
|
||||
'theme-color-users-1',
|
||||
'theme-color-users-2',
|
||||
'theme-color-users-3',
|
||||
'theme-color-users-4',
|
||||
'theme-color-users-5',
|
||||
'theme-color-users-6',
|
||||
'theme-color-users-7',
|
||||
]}
|
||||
/>
|
||||
</Story>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Design" parameters={{chromatic: { disableSnapshot: true }}}/>
|
||||
<Meta title="owncast/Documentation/Design" parameters={{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
# Owncast Design Guidelines & Resources
|
||||
|
||||
@@ -31,9 +31,20 @@ Read the detailed [product definition](https://github.com/owncast/owncast/blob/d
|
||||
|
||||
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.
|
||||
|
||||
## 🧑🎨 Product design opportunities
|
||||
|
||||
Owncast is a constantly moving project with features both old and new. This allows for design contributions to be both big or small.
|
||||
You may not know how much time you can dedicate to the project, or if you'll be able to see something through to the end, so be honest about that. Take on projects that you'll be able to see completed.
|
||||
|
||||
- So maybe start small by finding rough edges and improvements to existing features without requiring complete rewrites. As a small project the bandwidth for rebuilding existing designs is limited, but tweaks are appreciated. This is especially great if you don't know how much time or energy you'll be able to provide the project. If you think you have a week to help, but might not be around in a month small projects are better.
|
||||
- If you think you'll be around longer term, learn about future new features and start thinking about the design challenges of those so we can build them your feedback and design contributions in mind. See your designs put in the world through brand new functionality!
|
||||
- Not everything has to be a a feature. Think big picture. What can we start doing now to put the project in a better place six months from now, or a year?
|
||||
|
||||
## 💅 Design relevant materials
|
||||
|
||||
Here is a list of design relevant information and materials:
|
||||
A collection of design relevant information and materials can be found under the "style" section of "Storybook" here:
|
||||
|
||||
http://owncast.online/components
|
||||
|
||||
### Fonts
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Get Started with Owncast Development" parameters={{chromatic: { disableSnapshot: true }}}/>
|
||||
<Meta title="owncast/Documentation/Get Started with Owncast Development" parameters={{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
---
|
||||
title: "How to work on Owncast"
|
||||
|
||||
@@ -1,148 +1,324 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Canvas, Meta, Story, Description, IconGallery, IconItem } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Emoji" parameters={{chromatic: { disableSnapshot: true }}} />
|
||||
<Meta title="owncast/Frontend Assets/Emoji" parameters={{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# Built-in Custom Emoji
|
||||
|
||||
|
||||
<Description
|
||||
markdown={`
|
||||
## 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>
|
||||
<a href="img/emoji/blob/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
`} />
|
||||
|
||||
<IconGallery>
|
||||
<IconItem name="ablobattention.gif">
|
||||
<img src="img/emoji/blob/ablobattention.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobaww.gif">
|
||||
<img src="img/emoji/blob/ablobaww.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobblewobble.gif">
|
||||
<img src="img/emoji/blob/ablobblewobble.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobcheer.gif">
|
||||
<img src="img/emoji/blob/ablobcheer.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobcry.gif">
|
||||
<img src="img/emoji/blob/ablobcry.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobdancer.gif">
|
||||
<img src="img/emoji/blob/ablobdancer.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobgift.gif">
|
||||
<img src="img/emoji/blob/ablobgift.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobgiggle.gif">
|
||||
<img src="img/emoji/blob/ablobgiggle.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobparty.gif">
|
||||
<img src="img/emoji/blob/ablobparty.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobsleep.gif">
|
||||
<img src="img/emoji/blob/ablobsleep.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobthinking.gif">
|
||||
<img src="img/emoji/blob/ablobthinking.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="ablobwave.gif">
|
||||
<img src="img/emoji/blob/ablobwave.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="blobangry.png">
|
||||
<img src="img/emoji/blob/blobangry.png" />
|
||||
</IconItem>
|
||||
<IconItem name="blobaww.png">
|
||||
<img src="img/emoji/blob/blobaww.png" />
|
||||
</IconItem>
|
||||
<IconItem name="blobdancer.png">
|
||||
<img src="img/emoji/blob/blobdancer.png" />
|
||||
</IconItem>
|
||||
<IconItem name="blobjam.png">
|
||||
<img src="img/emoji/blob/blobjam.png" />
|
||||
</IconItem>
|
||||
<IconItem name="blobscream.png">
|
||||
<img src="img/emoji/blob/blobscream.png" />
|
||||
</IconItem>
|
||||
<IconItem name="blobthanks.png">
|
||||
<img src="img/emoji/blob/blobthanks.png" />
|
||||
</IconItem>
|
||||
<IconItem name="blobthinking.png">
|
||||
<img src="img/emoji/blob/blobthinking.png" />
|
||||
</IconItem>
|
||||
<IconItem name="blobwave.png">
|
||||
<img src="img/emoji/blob/blobwave.png" />
|
||||
</IconItem>
|
||||
<IconItem name="blobyes.png">
|
||||
<img src="img/emoji/blob/blobyes.png" />
|
||||
</IconItem>
|
||||
<IconItem name="blobyum.png">
|
||||
<img src="img/emoji/blob/blobyum.png" />
|
||||
</IconItem>
|
||||
</IconGallery>
|
||||
|
||||
|
||||
<Description
|
||||
markdown={`
|
||||
## 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>
|
||||
<a href="img/emoji/conigliolo96/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
`} />
|
||||
|
||||
<IconGallery>
|
||||
<IconItem name="conigliolo1.gif">
|
||||
<img src="img/emoji/conigliolo96/conigliolo1.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="conigliolo15.gif">
|
||||
<img src="img/emoji/conigliolo96/conigliolo15.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="conigliolo17.gif">
|
||||
<img src="img/emoji/conigliolo96/conigliolo17.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="conigliolo21.gif">
|
||||
<img src="img/emoji/conigliolo96/conigliolo21.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="conigliolo25.gif">
|
||||
<img src="img/emoji/conigliolo96/conigliolo25.gif" />
|
||||
</IconItem>
|
||||
<IconItem name="conigliolo28.gif">
|
||||
<img src="img/emoji/conigliolo96/conigliolo28.gif" />
|
||||
</IconItem>
|
||||
</IconGallery>
|
||||
|
||||
|
||||
<Description
|
||||
markdown={`
|
||||
## 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>
|
||||
<a href="img/emoji/dog/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
`} />
|
||||
|
||||
<IconGallery>
|
||||
<IconItem name="img001.svg">
|
||||
<img src="img/emoji/dog/img001.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="img091.svg">
|
||||
<img src="img/emoji/dog/img091.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="img093.svg">
|
||||
<img src="img/emoji/dog/img093.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="img203.svg">
|
||||
<img src="img/emoji/dog/img203.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="img288.svg">
|
||||
<img src="img/emoji/dog/img288.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="img327.svg">
|
||||
<img src="img/emoji/dog/img327.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="img346.svg">
|
||||
<img src="img/emoji/dog/img346.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="img347.svg">
|
||||
<img src="img/emoji/dog/img347.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="img352.svg">
|
||||
<img src="img/emoji/dog/img352.svg" />
|
||||
</IconItem>
|
||||
</IconGallery>
|
||||
|
||||
|
||||
<Description
|
||||
markdown={`
|
||||
## 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>
|
||||
<a href="img/emoji/mutant/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
`} />
|
||||
|
||||
<IconGallery>
|
||||
<IconItem name="8_ball.svg">
|
||||
<img src="img/emoji/mutant/8_ball.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="alien.svg">
|
||||
<img src="img/emoji/mutant/alien.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="american_football.svg">
|
||||
<img src="img/emoji/mutant/american_football.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="arms_in_the_air.svg">
|
||||
<img src="img/emoji/mutant/arms_in_the_air.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="artist.svg">
|
||||
<img src="img/emoji/mutant/artist.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="astronaut.svg">
|
||||
<img src="img/emoji/mutant/astronaut.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="back_of_hand_clw.svg">
|
||||
<img src="img/emoji/mutant/back_of_hand_clw.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="back_of_hand_hoof.svg">
|
||||
<img src="img/emoji/mutant/back_of_hand_hoof.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="back_of_hand_paw.svg">
|
||||
<img src="img/emoji/mutant/back_of_hand_paw.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="baseball.svg">
|
||||
<img src="img/emoji/mutant/baseball.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="basketball.svg">
|
||||
<img src="img/emoji/mutant/basketball.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="blep.svg">
|
||||
<img src="img/emoji/mutant/blep.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="bow_b3.svg">
|
||||
<img src="img/emoji/mutant/bow_b3.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="cat_crying.svg">
|
||||
<img src="img/emoji/mutant/cat_crying.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="cat_devious.svg">
|
||||
<img src="img/emoji/mutant/cat_devious.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="cat_grin.svg">
|
||||
<img src="img/emoji/mutant/cat_grin.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="cat_heart_eyes.svg">
|
||||
<img src="img/emoji/mutant/cat_heart_eyes.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="cat_joy.svg">
|
||||
<img src="img/emoji/mutant/cat_joy.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="cat_kiss.svg">
|
||||
<img src="img/emoji/mutant/cat_kiss.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="cat_pouting.svg">
|
||||
<img src="img/emoji/mutant/cat_pouting.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="cat_scream.svg">
|
||||
<img src="img/emoji/mutant/cat_scream.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="cat_smile.svg">
|
||||
<img src="img/emoji/mutant/cat_smile.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="chef.svg">
|
||||
<img src="img/emoji/mutant/chef.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="detective.svg">
|
||||
<img src="img/emoji/mutant/detective.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="ear.svg">
|
||||
<img src="img/emoji/mutant/ear.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="eye.svg">
|
||||
<img src="img/emoji/mutant/eye.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="eyes.svg">
|
||||
<img src="img/emoji/mutant/eyes.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="facepalm.svg">
|
||||
<img src="img/emoji/mutant/facepalm.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="football.svg">
|
||||
<img src="img/emoji/mutant/football.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="ghost.svg">
|
||||
<img src="img/emoji/mutant/ghost.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="grumpy_block.svg">
|
||||
<img src="img/emoji/mutant/grumpy_block.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="hot_shit.svg">
|
||||
<img src="img/emoji/mutant/hot_shit.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="jack_o_lantern.svg">
|
||||
<img src="img/emoji/mutant/jack_o_lantern.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="long_pointed_ear.svg">
|
||||
<img src="img/emoji/mutant/long_pointed_ear.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="mechanical_arm.svg">
|
||||
<img src="img/emoji/mutant/mechanical_arm.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="no_good.svg">
|
||||
<img src="img/emoji/mutant/no_good.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="office_worker.svg">
|
||||
<img src="img/emoji/mutant/office_worker.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="ok_gesture.svg">
|
||||
<img src="img/emoji/mutant/ok_gesture.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="person_frowning.svg">
|
||||
<img src="img/emoji/mutant/person_frowning.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="raising_hand.svg">
|
||||
<img src="img/emoji/mutant/raising_hand.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="robot.svg">
|
||||
<img src="img/emoji/mutant/robot.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="shrug.svg">
|
||||
<img src="img/emoji/mutant/shrug.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="singer.svg">
|
||||
<img src="img/emoji/mutant/singer.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="skull.svg">
|
||||
<img src="img/emoji/mutant/skull.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="skull_and_crossbones.svg">
|
||||
<img src="img/emoji/mutant/skull_and_crossbones.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="softball.svg">
|
||||
<img src="img/emoji/mutant/softball.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="student.svg">
|
||||
<img src="img/emoji/mutant/student.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="studio_microphone.svg">
|
||||
<img src="img/emoji/mutant/studio_microphone.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="technologist.svg">
|
||||
<img src="img/emoji/mutant/technologist.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="tennis.svg">
|
||||
<img src="img/emoji/mutant/tennis.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="volleyball.svg">
|
||||
<img src="img/emoji/mutant/volleyball.svg" />
|
||||
</IconItem>
|
||||
</IconGallery>
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
import { Canvas, Meta, Story, IconItem, IconGallery } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Images" parameters={{chromatic: { disableSnapshot: true }}} />
|
||||
<Meta title="owncast/Frontend Assets/Images" parameters={{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# Images
|
||||
|
||||
<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"},
|
||||
]}/>
|
||||
<IconGallery>
|
||||
<IconItem name="fediverse-black.png">
|
||||
<img src="img/fediverse-black.png" />
|
||||
</IconItem>
|
||||
<IconItem name="fediverse-color.png">
|
||||
<img src="img/fediverse-color.png" />
|
||||
</IconItem>
|
||||
<IconItem name="follow.svg">
|
||||
<img src="img/follow.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="indieauth.png">
|
||||
<img src="img/indieauth.png" />
|
||||
</IconItem>
|
||||
<IconItem name="like.svg">
|
||||
<img src="img/like.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="repost.svg">
|
||||
<img src="img/repost.svg" />
|
||||
</IconItem>
|
||||
</IconGallery>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Product Definition" parameters={{chromatic: { disableSnapshot: true }}}/>
|
||||
<Meta title="owncast/Documentation/Product Definition" parameters={{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
# Owncast Product Definition
|
||||
|
||||
|
||||
@@ -1,41 +1,102 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
import { Canvas, Meta, Story, IconItem, IconGallery } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Social Platform Images" parameters={{chromatic: { disableSnapshot: true }}} />
|
||||
<Meta title="owncast/Frontend Assets/Social Platform Images" parameters={{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# 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"},
|
||||
]}/>
|
||||
<IconGallery>
|
||||
<IconItem name="bandcamp.svg">
|
||||
<img src="img/platformlogos/bandcamp.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="default.svg">
|
||||
<img src="img/platformlogos/default.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="discord.svg">
|
||||
<img src="img/platformlogos/discord.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="donate.svg">
|
||||
<img src="img/platformlogos/donate.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="facebook.svg">
|
||||
<img src="img/platformlogos/facebook.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="fediverse.svg">
|
||||
<img src="img/platformlogos/fediverse.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="follow.svg">
|
||||
<img src="img/platformlogos/follow.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="github.svg">
|
||||
<img src="img/platformlogos/github.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="gitlab.svg">
|
||||
<img src="img/platformlogos/gitlab.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="google.svg">
|
||||
<img src="img/platformlogos/google.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="instagram.svg">
|
||||
<img src="img/platformlogos/instagram.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="keyoxide.png">
|
||||
<img src="img/platformlogos/keyoxide.png" />
|
||||
</IconItem>
|
||||
<IconItem name="ko-fi.svg">
|
||||
<img src="img/platformlogos/ko-fi.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="lbry.svg">
|
||||
<img src="img/platformlogos/lbry.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="liberapay.svg">
|
||||
<img src="img/platformlogos/liberapay.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="link.svg">
|
||||
<img src="img/platformlogos/link.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="linkedin.svg">
|
||||
<img src="img/platformlogos/linkedin.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="mastodon.svg">
|
||||
<img src="img/platformlogos/mastodon.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="matrix.svg">
|
||||
<img src="img/platformlogos/matrix.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="odysee.svg">
|
||||
<img src="img/platformlogos/odysee.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="patreon.svg">
|
||||
<img src="img/platformlogos/patreon.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="paypal.svg">
|
||||
<img src="img/platformlogos/paypal.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="snapchat.svg">
|
||||
<img src="img/platformlogos/snapchat.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="soundcloud.svg">
|
||||
<img src="img/platformlogos/soundcloud.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="spotify.svg">
|
||||
<img src="img/platformlogos/spotify.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="steam.svg">
|
||||
<img src="img/platformlogos/steam.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="tiktok.svg">
|
||||
<img src="img/platformlogos/tiktok.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="twitch.svg">
|
||||
<img src="img/platformlogos/twitch.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="twitter.svg">
|
||||
<img src="img/platformlogos/twitter.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="xmpp.svg">
|
||||
<img src="img/platformlogos/xmpp.svg" />
|
||||
</IconItem>
|
||||
<IconItem name="youtube.svg">
|
||||
<img src="img/platformlogos/youtube.svg" />
|
||||
</IconItem>
|
||||
</IconGallery>
|
||||
|
||||
|
||||
@@ -1,29 +1,146 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Canvas, Meta, Story, Typeset, Source } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Styles/Typography" />
|
||||
<Meta
|
||||
title="owncast/Styles/Typography"
|
||||
parameters={{
|
||||
previewTabs: { canvas: { hidden: true } },
|
||||
chromatic: { disableSnapshot: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
## Body
|
||||
export const SampleText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
|
||||
|
||||
<div style={{ fontSize: '1.2rem', fontFamily: 'var(--theme-text-body-font-family)' }}>
|
||||
The quick brown fox jumps over the lazy dog.
|
||||
</div>
|
||||
# Typography
|
||||
|
||||
These are the font families in use by Owncast.
|
||||
|
||||
---
|
||||
|
||||
export const bodyFont = {
|
||||
type: {
|
||||
primary: getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--theme-text-body-font-family',
|
||||
),
|
||||
},
|
||||
weight: {
|
||||
regular: '400',
|
||||
bold: '600',
|
||||
extrabold: '800',
|
||||
},
|
||||
size: {
|
||||
s1: 12,
|
||||
s2: 14,
|
||||
s3: 16,
|
||||
m1: 20,
|
||||
m2: 24,
|
||||
m3: 28,
|
||||
l1: 32,
|
||||
l2: 40,
|
||||
l3: 48,
|
||||
},
|
||||
};
|
||||
|
||||
## {bodyFont.type.primary.split(',')[0].replaceAll('"', '')}
|
||||
|
||||
### Everywhere but headings and titles.
|
||||
|
||||
<Typeset
|
||||
fontSizes={[
|
||||
Number(bodyFont.size.s1),
|
||||
Number(bodyFont.size.s2),
|
||||
Number(bodyFont.size.s3),
|
||||
Number(bodyFont.size.m1),
|
||||
Number(bodyFont.size.m2),
|
||||
Number(bodyFont.size.m3),
|
||||
Number(bodyFont.size.l1),
|
||||
Number(bodyFont.size.l2),
|
||||
Number(bodyFont.size.l3),
|
||||
]}
|
||||
fontWeight={bodyFont.weight.black}
|
||||
sampleText={SampleText}
|
||||
fontFamily={bodyFont.type.primary}
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
<Canvas
|
||||
style={{ color: 'var(--theme-text-secondary)', fontFamily: 'var(--theme-text-body-font-family)' }}
|
||||
columns={2}
|
||||
withSource="open"
|
||||
withToolbar
|
||||
style={{ fontFamily: 'var(--theme-text-body-font-family)' }}
|
||||
>
|
||||
{getComputedStyle(document.documentElement).getPropertyValue('--theme-text-body-font-family')}
|
||||
</Canvas>
|
||||
|
||||
## Display
|
||||
<Source
|
||||
language="css"
|
||||
dark
|
||||
format={true}
|
||||
code={`
|
||||
font-family: var(--theme-text-body-font-family);
|
||||
`}
|
||||
/>
|
||||
|
||||
export const displayFont = {
|
||||
type: {
|
||||
primary: getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--theme-text-display-font-family',
|
||||
),
|
||||
},
|
||||
weight: {
|
||||
regular: '400',
|
||||
bold: '600',
|
||||
extrabold: '800',
|
||||
},
|
||||
size: {
|
||||
s1: 12,
|
||||
s2: 14,
|
||||
s3: 16,
|
||||
m1: 20,
|
||||
m2: 24,
|
||||
m3: 28,
|
||||
l1: 32,
|
||||
l2: 40,
|
||||
l3: 48,
|
||||
},
|
||||
};
|
||||
|
||||
## {displayFont.type.primary.split(',')[0].replaceAll('"','')}
|
||||
|
||||
### Headings and titles.
|
||||
|
||||
<Typeset
|
||||
fontSizes={[
|
||||
Number(displayFont.size.s1),
|
||||
Number(displayFont.size.s2),
|
||||
Number(displayFont.size.s3),
|
||||
Number(displayFont.size.m1),
|
||||
Number(displayFont.size.m2),
|
||||
Number(displayFont.size.m3),
|
||||
Number(displayFont.size.l1),
|
||||
Number(displayFont.size.l2),
|
||||
Number(displayFont.size.l3),
|
||||
]}
|
||||
fontWeight={displayFont.weight.black}
|
||||
sampleText={SampleText}
|
||||
fontFamily={displayFont.type.primary}
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
<div style={{ fontSize: '1.2rem', fontFamily: 'var(--theme-text-display-font-family)' }}>
|
||||
The quick brown fox jumps over the lazy dog.
|
||||
</div>
|
||||
<Canvas
|
||||
style={{
|
||||
color: 'var(--theme-text-secondary)',
|
||||
fontFamily: 'var(--theme-text-display-font-family)',
|
||||
}}
|
||||
columns={2}
|
||||
withSource="open"
|
||||
withToolbarstyle={{ fontFamily: 'var(--theme-text-display-font-family)' }}
|
||||
>
|
||||
{getComputedStyle(document.documentElement).getPropertyValue('--theme-text-display-font-family')}
|
||||
</Canvas>
|
||||
|
||||
<Source
|
||||
language="css"
|
||||
dark
|
||||
format={true}
|
||||
code={`
|
||||
font-family: var(--theme-text-display-font-family);
|
||||
`}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Building Frontend Components" parameters={{chromatic: { disableSnapshot: true }}}/>
|
||||
<Meta title="owncast/Documentation/Building Frontend Components" parameters={{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
# How we develop components
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { FC } from 'react';
|
||||
|
||||
export type ColorProps = {
|
||||
color: any; // TODO specify better type
|
||||
color: string;
|
||||
};
|
||||
|
||||
export const Color: FC<ColorProps> = ({ color }) => {
|
||||
@@ -55,10 +54,6 @@ export const Color: FC<ColorProps> = ({ color }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Color.propTypes = {
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const rowStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row' as 'row',
|
||||
@@ -66,7 +61,11 @@ const rowStyle = {
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
export const ColorRow = props => {
|
||||
export type ColorRowProps = {
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
export const ColorRow: FC<ColorRowProps> = (props: ColorRowProps) => {
|
||||
const { colors } = props;
|
||||
|
||||
return (
|
||||
@@ -78,6 +77,8 @@ export const ColorRow = props => {
|
||||
);
|
||||
};
|
||||
|
||||
ColorRow.propTypes = {
|
||||
colors: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
export const getColor = color => {
|
||||
const colorValue = getComputedStyle(document.documentElement).getPropertyValue(`--${color}`);
|
||||
return { [color]: colorValue };
|
||||
// return { [color]: colorValue };
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Canvas, Meta, Story, ColorPalette, ColorItem, Description } from '@storybook/addon-docs';
|
||||
import { ColorRow, getColor } from './Color';
|
||||
|
||||
<Meta
|
||||
title="owncast/Styles/Colors/Components"
|
||||
parameters={{
|
||||
previewTabs: { canvas: { hidden: true } },
|
||||
chromatic: { disableSnapshot: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Description markdown={`
|
||||
|
||||
## Component Colors
|
||||
|
||||
These are the specific colors used for components in the web application. They point to colors in the Owncast color palette but CSS variable names can be overwritten for customizing the theme.
|
||||
|
||||
`}/>
|
||||
|
||||
<ColorPalette>
|
||||
<ColorItem
|
||||
title="Text"
|
||||
subtitle=""
|
||||
colors={{
|
||||
...getColor('theme-color-components-text-on-light'),
|
||||
...getColor('theme-color-components-text-on-dark'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="Primary Button"
|
||||
subtitle=""
|
||||
colors={{
|
||||
...getColor('theme-color-components-primary-button-background'),
|
||||
...getColor('theme-color-components-primary-button-background-disabled'),
|
||||
...getColor('theme-color-components-primary-button-text'),
|
||||
...getColor('theme-color-components-primary-button-text-disabled'),
|
||||
...getColor('theme-color-components-primary-button-border')
|
||||
}}
|
||||
/>
|
||||
|
||||
<ColorItem
|
||||
title="Secondary Button"
|
||||
subtitle=""
|
||||
colors={{
|
||||
...getColor('theme-color-components-secondary-button-background'),
|
||||
...getColor('theme-color-components-secondary-button-background-disabled'),
|
||||
...getColor('theme-color-components-secondary-button-text'),
|
||||
...getColor('theme-color-components-secondary-button-text-disabled'),
|
||||
...getColor('theme-color-components-secondary-button-border'),
|
||||
}}
|
||||
/>
|
||||
|
||||
<ColorItem
|
||||
title="Chat"
|
||||
subtitle=""
|
||||
colors={{
|
||||
...getColor('theme-color-components-chat-background'),
|
||||
...getColor('theme-color-components-chat-text'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="Modals"
|
||||
subtitle=""
|
||||
colors={{
|
||||
...getColor('theme-color-components-modal-header-background'),
|
||||
...getColor('theme-color-components-modal-header-text'),
|
||||
...getColor('theme-color-components-modal-content-background'),
|
||||
...getColor('theme-color-components-modal-content-text'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="Page Content"
|
||||
subtitle="Tabbed content"
|
||||
colors={{
|
||||
...getColor('theme-color-components-content-background'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="Menus"
|
||||
subtitle=""
|
||||
colors={{
|
||||
...getColor('theme-color-components-menu-background'),
|
||||
...getColor('theme-color-components-menu-item-text'),
|
||||
...getColor('theme-color-components-menu-item-bg'),
|
||||
...getColor('theme-color-components-menu-item-text'),
|
||||
...getColor('theme-color-components-menu-item-hover-bg'),
|
||||
...getColor('theme-color-components-menu-item-focus-bg'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="Form Fields"
|
||||
subtitle=""
|
||||
colors={{
|
||||
...getColor('theme-color-components-form-field-background'),
|
||||
...getColor('theme-color-components-form-field-placeholder'),
|
||||
...getColor('theme-color-components-form-field-text'),
|
||||
...getColor('theme-color-components-menu-item-text'),
|
||||
...getColor('theme-color-components-form-field-border'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="Video Player Status Bar"
|
||||
subtitle="Displays duration and viewer count."
|
||||
colors={{
|
||||
...getColor('theme-color-components-video-status-bar-background'),
|
||||
...getColor('theme-color-components-video-status-bar-foreground'),
|
||||
}}
|
||||
/>
|
||||
|
||||
</ColorPalette>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Canvas, Meta, Story, ColorPalette, ColorItem, Description } from '@storybook/addon-docs';
|
||||
import { ColorRow, getColor } from './Color';
|
||||
|
||||
<Meta
|
||||
title="owncast/Styles/Colors/Palette"
|
||||
parameters={{
|
||||
previewTabs: { canvas: { hidden: true } },
|
||||
chromatic: { disableSnapshot: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Description markdown={`
|
||||
|
||||
## Theme Color Palette
|
||||
|
||||
These are the colors used across the web application. All the specific component colors point to colors in this palette. If you override one of these colors all usage of that color will be updated.
|
||||
|
||||
`}/>
|
||||
|
||||
<ColorPalette>
|
||||
<ColorItem
|
||||
title="Theme color palette"
|
||||
subtitle="Colors used across the theme."
|
||||
colors={{
|
||||
...getColor('theme-color-palette-0'),
|
||||
...getColor('theme-color-palette-1'),
|
||||
...getColor('theme-color-palette-2'),
|
||||
...getColor('theme-color-palette-3'),
|
||||
...getColor('theme-color-palette-4'),
|
||||
...getColor('theme-color-palette-5'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
colors={{
|
||||
...getColor('theme-color-palette-6'),
|
||||
...getColor('theme-color-palette-7'),
|
||||
...getColor('theme-color-palette-8'),
|
||||
...getColor('theme-color-palette-9'),
|
||||
...getColor('theme-color-palette-10'),
|
||||
...getColor('theme-color-palette-11'),
|
||||
}}
|
||||
|
||||
/>
|
||||
|
||||
<ColorItem
|
||||
colors={{
|
||||
...getColor('theme-color-palette-12'),
|
||||
...getColor('theme-color-palette-13'),
|
||||
...getColor('theme-color-palette-14'),
|
||||
...getColor('theme-color-palette-15'),
|
||||
...getColor('theme-color-palette-error'),
|
||||
...getColor('theme-color-palette-warning'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
colors={{
|
||||
...getColor('theme-color-background-light'),
|
||||
...getColor('theme-color-background-header'),
|
||||
...getColor('theme-color-action'),
|
||||
...getColor('theme-color-action-hover'),
|
||||
...getColor('theme-color-action-disabled'),
|
||||
}}
|
||||
/>
|
||||
</ColorPalette>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Canvas, Meta, Story, ColorPalette, ColorItem, Description } from '@storybook/addon-docs';
|
||||
import { ColorRow, getColor } from './Color';
|
||||
|
||||
<Meta
|
||||
title="owncast/Styles/Colors/Users"
|
||||
parameters={{
|
||||
previewTabs: { canvas: { hidden: true } },
|
||||
chromatic: { disableSnapshot: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Description markdown={`
|
||||
|
||||
## User Colors
|
||||
|
||||
These are the colors available for assigning to chat users for display purposes. The CSS variables can be overwritten for customizing the theme.
|
||||
|
||||
`}/>
|
||||
|
||||
<ColorPalette>
|
||||
<ColorItem title="User Color 0" colors={{ ...getColor('theme-color-users-0') }} />
|
||||
<ColorItem
|
||||
title="User Color 1"
|
||||
colors={{
|
||||
...getColor('theme-color-users-1'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="User Color 2"
|
||||
colors={{
|
||||
...getColor('theme-color-users-2'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="User Color 3"
|
||||
colors={{
|
||||
...getColor('theme-color-users-3'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="User Color 4"
|
||||
colors={{
|
||||
...getColor('theme-color-users-4'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="User Color 5"
|
||||
colors={{
|
||||
...getColor('theme-color-users-5'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="User Color 6"
|
||||
colors={{
|
||||
...getColor('theme-color-users-6'),
|
||||
}}
|
||||
/>
|
||||
<ColorItem
|
||||
title="User Color 7"
|
||||
colors={{
|
||||
...getColor('theme-color-users-7'),
|
||||
}}
|
||||
/>
|
||||
</ColorPalette>
|
||||
@@ -1,14 +1,20 @@
|
||||
import { Mermaid } from 'mdx-mermaid/Mermaid';
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Canvas, Meta, Story, Description } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Documentation/Usage Examples"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
title="owncast/Documentation/Infrastructure Examples/Basic Example"
|
||||
parameters={{
|
||||
previewTabs: { canvas: { hidden: true } },
|
||||
chromatic: { disableSnapshot: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story name="Basic">
|
||||
<Description
|
||||
markdown={`
|
||||
## Basic
|
||||
|
||||
This is the most basic Owncast setup and is what you get when you run it without any additional services.
|
||||
`} />
|
||||
|
||||
<Mermaid
|
||||
chart={`graph TD
|
||||
@@ -28,12 +34,13 @@ This is the most basic Owncast setup and is what you get when you run it without
|
||||
`}
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="S3 Object Storage">
|
||||
<Description
|
||||
markdown={`
|
||||
## S3 Object Storage
|
||||
|
||||
If you would like no video traffic to take place from your Owncast server to your clients, you can use S3 Object Storage to be responsible for this instead.
|
||||
Instead of video players pulling the stream from your Owncast server it would instead make these requests to a S3 provider, and Owncast would upload video to this provider to make it available.
|
||||
`} />
|
||||
|
||||
<Mermaid
|
||||
chart={`graph TD
|
||||
@@ -67,13 +74,15 @@ Instead of video players pulling the stream from your Owncast server it would in
|
||||
`}
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="CDN in front of S3 Object Storage">
|
||||
<Description
|
||||
markdown={`
|
||||
## CDN in front of S3 Object Storage
|
||||
|
||||
If you're using a S3 provider but would like to increase the speed of your video delivery to your clients around the world, you can use a CDN in front of your S3 provider.
|
||||
This leads to not only your server not serving any video traffic, but the video traffic would be distributed globally so each client player would be pulling video from somewhere geographically closer, reducing the likelihood of buffering and slower network requests.
|
||||
|
||||
`} />
|
||||
|
||||
<Mermaid
|
||||
chart={`flowchart TD
|
||||
subgraph Video Assets
|
||||
@@ -110,11 +119,12 @@ This leads to not only your server not serving any video traffic, but the video
|
||||
`}
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="CDN in front of Owncast">
|
||||
<Description
|
||||
markdown={`
|
||||
## CDN in front of Owncast
|
||||
|
||||
If you're ok with some video requests coming directly to your Owncast server, and want to generally increase the overall speed that your viewers globally access all your Owncast assets, including video and the web interface, you can put a CDN in front of your entire Owncast server.
|
||||
`} />
|
||||
|
||||
<Mermaid
|
||||
chart={`flowchart TD
|
||||
@@ -143,5 +153,3 @@ If you're ok with some video requests coming directly to your Owncast server, an
|
||||
|
||||
`}
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Mermaid } from 'mdx-mermaid/Mermaid';
|
||||
import { Canvas, Meta, Story, Description } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Documentation/Infrastructure Examples/S3 With CDN Example"
|
||||
parameters={{
|
||||
previewTabs: { canvas: { hidden: true } },
|
||||
chromatic: { disableSnapshot: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Description
|
||||
markdown={`
|
||||
## CDN in front of S3 Object Storage
|
||||
|
||||
If you're using a S3 provider but would like to increase the speed of your video delivery to your clients around the world, you can use a CDN in front of your S3 provider.
|
||||
This leads to not only your server not serving any video traffic, but the video traffic would be distributed globally so each client player would be pulling video from somewhere geographically closer, reducing the likelihood of buffering and slower network requests.
|
||||
|
||||
`} />
|
||||
|
||||
<Mermaid
|
||||
chart={`flowchart TD
|
||||
subgraph Video Assets
|
||||
S3[fa:fa-hard-drive S3 Object Storage]
|
||||
CDN[fa:fa-cloud Global CDN]
|
||||
end
|
||||
|
||||
subgraph Clients
|
||||
WebPlayer[fa:fa-window-maximize Web App]
|
||||
Embeds[fa:fa-window-restore Embeds]
|
||||
MobileApps[fa:fa-mobile-screen Mobile Apps]
|
||||
SmartTV[fa:fa-tv Smart TV]
|
||||
VLC[fa:fa-shapes VLC]
|
||||
end
|
||||
|
||||
subgraph Web Assets & Chat Service
|
||||
direction TB
|
||||
Owncast{fa:fa-server Owncast}
|
||||
end
|
||||
|
||||
Owncast--Upload\\nVideo-->S3
|
||||
Owncast--Web-->WebPlayer
|
||||
Owncast<--Chat-->WebPlayer
|
||||
Owncast--Web-->Embeds
|
||||
|
||||
CDN--Download\\nVideo-->WebPlayer
|
||||
CDN--Download\\nVideo-->Embeds
|
||||
CDN--Download\\nVideo-->MobileApps
|
||||
CDN--Download\\nVideo-->SmartTV
|
||||
CDN--Download\\nVideo-->VLC
|
||||
|
||||
S3 --- CDN
|
||||
|
||||
`}
|
||||
/>
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Mermaid } from 'mdx-mermaid/Mermaid';
|
||||
import { Canvas, Meta, Story, Description } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Documentation/Infrastructure Examples/CDN Example"
|
||||
parameters={{
|
||||
previewTabs: { canvas: { hidden: true } },
|
||||
chromatic: { disableSnapshot: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Description
|
||||
markdown={`
|
||||
## CDN in front of Owncast
|
||||
|
||||
If you're ok with some video requests coming directly to your Owncast server, and want to generally increase the overall speed that your viewers globally access all your Owncast assets, including video and the web interface, you can put a CDN in front of your entire Owncast server.
|
||||
`} />
|
||||
|
||||
<Mermaid
|
||||
chart={`flowchart TD
|
||||
CDN{{fa:fa-cloud Global CDN}}
|
||||
Owncast{fa:fa-server Owncast}
|
||||
|
||||
subgraph Clients
|
||||
WebPlayer[fa:fa-window-maximize Web App]
|
||||
Embeds[fa:fa-window-restore Embeds]
|
||||
MobileApps[fa:fa-mobile-screen Mobile Apps]
|
||||
SmartTV[fa:fa-tv Smart TV]
|
||||
VLC[fa:fa-shapes VLC]
|
||||
end
|
||||
|
||||
CDN--Web-->WebPlayer
|
||||
Owncast<--Chat-->WebPlayer
|
||||
CDN--Web-->Embeds
|
||||
|
||||
CDN--Download\\nVideo-->WebPlayer
|
||||
CDN--Download\\nVideo-->Embeds
|
||||
CDN--Download\\nVideo-->MobileApps
|
||||
CDN--Download\\nVideo-->SmartTV
|
||||
CDN--Download\\nVideo-->VLC
|
||||
|
||||
CDN --- Owncast
|
||||
|
||||
`}
|
||||
/>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Mermaid } from 'mdx-mermaid/Mermaid';
|
||||
import { Canvas, Meta, Story, Description } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Documentation/Infrastructure Examples/S3 Example"
|
||||
parameters={{
|
||||
previewTabs: { canvas: { hidden: true } },
|
||||
chromatic: { disableSnapshot: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Description
|
||||
markdown={`
|
||||
## S3 Object Storage
|
||||
|
||||
If you would like no video traffic to take place from your Owncast server to your clients, you can use S3 Object Storage to be responsible for this instead.
|
||||
Instead of video players pulling the stream from your Owncast server it would instead make these requests to a S3 provider, and Owncast would upload video to this provider to make it available.
|
||||
`} />
|
||||
|
||||
<Mermaid
|
||||
chart={`graph TD
|
||||
subgraph Chat & Web Assets
|
||||
Owncast{fa:fa-server Owncast}
|
||||
end
|
||||
|
||||
Owncast--Upload\\nVideo-->S3
|
||||
|
||||
subgraph Video Assets
|
||||
S3[fa:fa-hard-drive S3 Object Storage]
|
||||
end
|
||||
|
||||
subgraph Clients
|
||||
WebPlayer[fa:fa-window-maximize Web App]
|
||||
Embeds[fa:fa-window-restore Embeds]
|
||||
MobileApps[fa:fa-mobile-screen Mobile Apps]
|
||||
SmartTV[fa:fa-tv Smart TV]
|
||||
VLC[fa:fa-shapes VLC]
|
||||
end
|
||||
|
||||
Owncast--Web-->WebPlayer
|
||||
Owncast--Web-->Embeds
|
||||
Owncast<--Chat-->WebPlayer
|
||||
S3--Download\\nVideo-->WebPlayer
|
||||
S3--Download\\nVideo-->Embeds
|
||||
S3--Download\\nVideo-->MobileApps
|
||||
S3--Download\\nVideo-->SmartTV
|
||||
S3--Download\\nVideo-->VLC
|
||||
|
||||
`}
|
||||
/>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/Android Landscape/Stock Browser"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/android-10.0-android-browser-samsung-galaxy-s20-ultra-landscape-offline.png"
|
||||
alt="Android Browser offline"
|
||||
height="1000px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/android-10.0-android-browser-samsung-galaxy-s20-ultra-landscape-online.png"
|
||||
alt="Android Browser offline"
|
||||
height="1000px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/Android Portrait/Stock Browser"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/android-13.0-android-browser-google-pixel-7-pro-portrait-offline.png"
|
||||
alt="Android Browser offline"
|
||||
height="1000px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/android-13.0-android-browser-google-pixel-7-pro-portrait-online.png"
|
||||
alt="Android Browser offline"
|
||||
height="1000px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/iPad Landscape/Safari"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/ios-16-mobile-safari-ipad-pro-11-2022-landscape-offline.png"
|
||||
alt="macOS Safari offline"
|
||||
width="800px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/ios-16-mobile-safari-ipad-pro-11-2022-landscape-online.png"
|
||||
alt="macOS Safari online"
|
||||
width="800px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/iPad Portrait/Safari"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/ios-16-mobile-safari-ipad-pro-11-2022-portrait-offline.png"
|
||||
alt="macOS Safari offline"
|
||||
width="800px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/ios-16-mobile-safari-ipad-pro-11-2022-portrait-online.png"
|
||||
alt="macOS Safari online"
|
||||
width="800px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/iPhone/Safari/Landscape"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/ios-16-mobile-safari-ipad-pro-11-2022-landscape-offline.png"
|
||||
alt="iPhone Safari offline"
|
||||
height="1000px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/ios-16-mobile-safari-ipad-pro-11-2022-landscape-online.png"
|
||||
alt="iPhone Safari online"
|
||||
height="1000px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/iPhone/Safari/Portrait"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/ios-16-mobile-safari-ipad-pro-11-2022-landscape-offline.png"
|
||||
alt="iPhone Safari offline"
|
||||
height="1000px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/ios-16-mobile-safari-ipad-pro-11-2022-landscape-online.png"
|
||||
alt="iPhone Safari online"
|
||||
height="1000px"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/macOS/Chrome"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/os-x-ventura-chrome-desktop-default-offline.png"
|
||||
alt="macOS Chrome offline"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/os-x-ventura-chrome-desktop-default-online.png"
|
||||
alt="macOS Chrome online"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/macOS/Firefox"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/os-x-ventura-firefox-desktop-default-offline.png"
|
||||
alt="macOS Chrome offline"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/os-x-ventura-firefox-desktop-default-online.png"
|
||||
alt="macOS Chrome online"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/macOS/Safari"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/os-x-ventura-safari-desktop-default-offline.png"
|
||||
alt="macOS Safari offline"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/os-x-ventura-safari-desktop-default-online.png"
|
||||
alt="macOS Safari online"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/Windows/Chrome"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/windows-11-chrome-desktop-default-offline.png"
|
||||
alt="Windows Chrome offline"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/windows-11-chrome-desktop-default-online.png"
|
||||
alt="Windows Chrome online"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Screenshots/Windows/Firefox"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Offline">
|
||||
|
||||
<img
|
||||
src="screenshots/windows-10-firefox-desktop-default-offline.png"
|
||||
alt="Windows Firefox offline"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="Online">
|
||||
|
||||
<img
|
||||
src="screenshots/windows-10-firefox-desktop-default-online.png"
|
||||
alt="Windows Firefox online"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -1,5 +1,5 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/{{title}}" parameters=\{{chromatic: { disableSnapshot: true }}}/>
|
||||
<Meta title="owncast/Documentation/{{title}}" parameters=\{{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
{{content}}
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Canvas, Meta, Story, Description, IconGallery, IconItem } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Emoji" parameters=\{{chromatic: { disableSnapshot: true }}} />
|
||||
<Meta title="owncast/Frontend Assets/Emoji" parameters=\{{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# Built-in Custom Emoji
|
||||
|
||||
{{#each emojiCollections}}
|
||||
|
||||
<Description
|
||||
markdown={`
|
||||
## {{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>
|
||||
<a href="img/emoji/{{this.name}}/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
`} />
|
||||
|
||||
<IconGallery>
|
||||
{{#each images}}
|
||||
<IconItem name="{{this.name}}">
|
||||
<img src="{{this.src}}" />
|
||||
</IconItem>
|
||||
{{/each}}
|
||||
</IconGallery>
|
||||
|
||||
{{/each}}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
import { Canvas, Meta, Story, IconItem, IconGallery } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="{{category}}" parameters=\{{chromatic: { disableSnapshot: true }}} />
|
||||
<Meta title="{{category}}" parameters=\{{previewTabs: { canvas: { hidden: true } }, chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# {{capitalize title}}
|
||||
|
||||
<ImageRow images={[
|
||||
{{#each images}}
|
||||
{src: "{{this.src}}", name: "{{this.name}}"},
|
||||
{{/each}}
|
||||
]}/>
|
||||
<IconGallery>
|
||||
{{#each images}}
|
||||
<IconItem name="{{this.name}}">
|
||||
<img src="{{this.src}}" />
|
||||
</IconItem>
|
||||
{{/each}}
|
||||
</IconGallery>
|
||||
|
||||
12
web/.storybook/tools/ImagesLarge.stories.mdx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="{{category}}" parameters=\{{chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# {{capitalize title}}
|
||||
|
||||
<ImageRow images={[
|
||||
{{#each images}}
|
||||
{src: "{{this.src}}", name: "{{this.name}}"},
|
||||
{{/each}}
|
||||
]}/>
|
||||
@@ -13,6 +13,7 @@ const dir = args[2];
|
||||
const title = args[3];
|
||||
const category = args[4];
|
||||
const publicPath = args[5];
|
||||
const useLarge = args[6];
|
||||
|
||||
if (args.length < 6) {
|
||||
console.error('Usage: generate-image-story.mjs <dir> <title> <category> <webpublicpath>');
|
||||
@@ -30,7 +31,8 @@ const images = readdirSync(dir)
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const template = fs.readFileSync('./Images.stories.mdx', 'utf8');
|
||||
const templateFile = useLarge ? './ImagesLarge.stories.mdx' : './Images.stories.mdx';
|
||||
const template = fs.readFileSync(templateFile, 'utf8');
|
||||
let t = handlebars.compile(template);
|
||||
let output = t({ images, title, category });
|
||||
console.log(output);
|
||||
|
||||
@@ -13,5 +13,5 @@ node generate-document-stories.mjs
|
||||
|
||||
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
|
||||
node generate-image-story.mjs ../story-assets/project/ "Logos & Graphics" "owncast/Project Assets/Logos & Graphics" "project" --large >../stories-category-doc-pages/LogosAndGraphics.stories.mdx
|
||||
node generate-image-story.mjs ../story-assets/tshirt/ "T-shirt" "owncast/Project Assets/T-Shirt" "tshirt" --large >../stories-category-doc-pages/Tshirt.stories.mdx
|
||||
|
||||
@@ -151,7 +151,9 @@ export const CurrentVariantsTable: FC = () => {
|
||||
dataIndex: 'cpuUsageLevel',
|
||||
key: 'cpuUsageLevel',
|
||||
render: (level: string, variant: VideoVariant) =>
|
||||
!level || variant.videoPassthrough ? 'n/a' : ENCODER_PRESET_TOOLTIPS[level].split(' ')[0],
|
||||
!level || variant.videoPassthrough
|
||||
? 'n/a'
|
||||
: ENCODER_PRESET_TOOLTIPS[level]?.split(' ')[0] || 'Warning: please edit & reset',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
|
||||
@@ -68,7 +68,7 @@ export const EditCustomJavascript: FC = () => {
|
||||
setContent(initialContent);
|
||||
}, [instanceDetails]);
|
||||
|
||||
const onCSSValueChange = React.useCallback(value => {
|
||||
const onValueChange = React.useCallback(value => {
|
||||
setContent(value);
|
||||
if (value !== initialContent && !hasChanged) {
|
||||
setHasChanged(true);
|
||||
@@ -80,20 +80,18 @@ export const EditCustomJavascript: FC = () => {
|
||||
return (
|
||||
<div className="edit-custom-css">
|
||||
<Title level={3} className="section-title">
|
||||
Customize your page styling with CSS
|
||||
Customize your page with Javascript
|
||||
</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{' '}
|
||||
Insert custom Javascript into your Owncast page to add your own functionality or to add 3rd
|
||||
party scripts. Read more about how to use this feature in the{' '}
|
||||
<a href="https://owncast.online/docs/website/" rel="noopener noreferrer" target="_blank">
|
||||
CSS & Components guide
|
||||
Web page documentation.
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className="description">
|
||||
Please input plain CSS text, as this will be directly injected onto your page during load.
|
||||
</p>
|
||||
<p className="description">Please use raw Javascript, no HTML or any script tags.</p>
|
||||
|
||||
<CodeMirror
|
||||
value={content}
|
||||
@@ -101,7 +99,7 @@ export const EditCustomJavascript: FC = () => {
|
||||
theme={bbedit}
|
||||
height="200px"
|
||||
extensions={[javascript()]}
|
||||
onChange={onCSSValueChange}
|
||||
onChange={onValueChange}
|
||||
/>
|
||||
|
||||
<br />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { Button, Collapse, Typography, Tooltip } from 'antd';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Collapse, Typography } from 'antd';
|
||||
import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD, TEXTFIELD_TYPE_URL } from './TextField';
|
||||
import { TextFieldWithSubmit } from './TextFieldWithSubmit';
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
@@ -15,16 +14,6 @@ import {
|
||||
import { UpdateArgs } from '../../types/config-section';
|
||||
import { ResetYP } from './ResetYP';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
const CopyOutlined = dynamic(() => import('@ant-design/icons/CopyOutlined'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const RedoOutlined = dynamic(() => import('@ant-design/icons/RedoOutlined'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
@@ -38,10 +27,6 @@ export default function EditInstanceDetails() {
|
||||
const { adminPassword, ffmpegPath, rtmpServerPort, webServerPort, yp, socketHostOverride } =
|
||||
serverConfig;
|
||||
|
||||
const [copyIsVisible, setCopyVisible] = useState(false);
|
||||
|
||||
const COPY_TOOLTIP_TIMEOUT = 3000;
|
||||
|
||||
useEffect(() => {
|
||||
setFormDataValues({
|
||||
adminPassword,
|
||||
@@ -79,22 +64,6 @@ export default function EditInstanceDetails() {
|
||||
}
|
||||
};
|
||||
|
||||
function generateStreamKey() {
|
||||
let key = '';
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
key += Math.random().toString(36).substring(2);
|
||||
}
|
||||
|
||||
handleFieldChange({ fieldName: 'streamKey', value: key });
|
||||
}
|
||||
|
||||
function copyStreamKey() {
|
||||
navigator.clipboard.writeText(formDataValues.streamKey).then(() => {
|
||||
setCopyVisible(true);
|
||||
setTimeout(() => setCopyVisible(false), COPY_TOOLTIP_TIMEOUT);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="edit-server-details-container">
|
||||
<div className="field-container field-streamkey-container">
|
||||
@@ -108,18 +77,6 @@ export default function EditInstanceDetails() {
|
||||
onChange={handleFieldChange}
|
||||
onSubmit={showStreamKeyChangeMessage}
|
||||
/>
|
||||
<div className="streamkey-actions">
|
||||
<Tooltip title="Generate a stream key">
|
||||
<Button icon={<RedoOutlined />} size="small" onClick={generateStreamKey} />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
className="copy-tooltip"
|
||||
title={copyIsVisible ? 'Copied!' : 'Copy to clipboard'}
|
||||
>
|
||||
<Button icon={<CopyOutlined />} size="small" onClick={copyStreamKey} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TextFieldWithSubmit
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { FC, ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Link from 'next/link';
|
||||
import Head from 'next/head';
|
||||
import { differenceInSeconds } from 'date-fns';
|
||||
@@ -224,7 +223,7 @@ export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
|
||||
key: 'viewer-info',
|
||||
},
|
||||
!chatDisabled && {
|
||||
label: <Link href="/admin/viewer-info">Chat & Users</Link>,
|
||||
label: <span>Chat & Users</span>,
|
||||
icon: <MessageOutlined />,
|
||||
children: chatMenu,
|
||||
key: 'chat-and-users',
|
||||
@@ -338,7 +337,3 @@ export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
MainLayout.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
|
||||
const { serverConfig } = serverStatusData || {};
|
||||
const { rtmpServerPort } = serverConfig;
|
||||
const { rtmpServerPort, streamKeyOverridden } = serverConfig;
|
||||
const instanceUrl = global.window?.location.hostname || '';
|
||||
|
||||
let rtmpURL;
|
||||
@@ -79,7 +79,13 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
|
||||
Streaming Keys:
|
||||
</Text>
|
||||
<Text strong className="stream-info-box">
|
||||
<Link href="/admin/config/server"> View </Link>
|
||||
{!streamKeyOverridden ? (
|
||||
<Link href="/admin/config/server"> View </Link>
|
||||
) : (
|
||||
<span style={{ paddingLeft: '10px', fontWeight: 'normal' }}>
|
||||
Overridden via command line.
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -159,8 +159,8 @@ export const VideoVariantForm: FC<VideoVariantFormProps> = ({
|
||||
<Slider
|
||||
tipFormatter={value => ENCODER_PRESET_TOOLTIPS[value]}
|
||||
onChange={handleVideoCpuUsageLevelChange}
|
||||
min={1}
|
||||
max={Object.keys(ENCODER_PRESET_SLIDER_MARKS).length}
|
||||
min={0}
|
||||
max={Object.keys(ENCODER_PRESET_SLIDER_MARKS).length - 1}
|
||||
marks={ENCODER_PRESET_SLIDER_MARKS}
|
||||
defaultValue={dataState.cpuUsageLevel}
|
||||
value={dataState.cpuUsageLevel}
|
||||
|
||||
@@ -87,7 +87,7 @@ const AddKeyForm = ({ setShowAddKeyForm, setFieldInConfigState, streamKeys, setE
|
||||
setHasChanged(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Default auto-generated key
|
||||
const defaultKey = generateRndKey();
|
||||
|
||||
|
||||
@@ -14,23 +14,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.nameChangeView {
|
||||
display: flex;
|
||||
font-size: 0.9rem;
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
padding: 5px 15px;
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
background-color: var(--color-owncast-background);
|
||||
& .nameChangeText {
|
||||
font-weight: bold;
|
||||
font-family: var(--theme-text-display-font-family);
|
||||
& .plain {
|
||||
font-weight: normal;
|
||||
font-family: var(--theme-text-body-font-family) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chatContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { useState, useMemo, useRef, CSSProperties, FC, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import {
|
||||
ConnectedClientInfoEvent,
|
||||
FediverseEvent,
|
||||
MessageType,
|
||||
NameChangeEvent,
|
||||
} from '../../../interfaces/socket-events';
|
||||
@@ -11,17 +11,13 @@ import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import { ChatUserMessage } from '../ChatUserMessage/ChatUserMessage';
|
||||
import { ChatTextField } from '../ChatTextField/ChatTextField';
|
||||
import { ChatModeratorNotification } from '../ChatModeratorNotification/ChatModeratorNotification';
|
||||
// import ChatActionMessage from '../ChatAction/ChatActionMessage';
|
||||
import { ChatSystemMessage } from '../ChatSystemMessage/ChatSystemMessage';
|
||||
import { ChatJoinMessage } from '../ChatJoinMessage/ChatJoinMessage';
|
||||
import { ScrollToBotBtn } from './ScrollToBotBtn';
|
||||
import { ChatActionMessage } from '../ChatActionMessage/ChatActionMessage';
|
||||
import { ChatSocialMessage } from '../ChatSocialMessage/ChatSocialMessage';
|
||||
import { ChatNameChangeMessage } from '../ChatNameChangeMessage/ChatNameChangeMessage';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
const EditFilled = dynamic(() => import('@ant-design/icons/EditFilled'), {
|
||||
ssr: false,
|
||||
});
|
||||
export type ChatContainerProps = {
|
||||
messages: ChatMessage[];
|
||||
usernameToHighlight: string;
|
||||
@@ -31,7 +27,11 @@ export type ChatContainerProps = {
|
||||
height?: string;
|
||||
};
|
||||
|
||||
function shouldCollapseMessages(messages: ChatMessage[], index: number): boolean {
|
||||
function shouldCollapseMessages(
|
||||
messages: ChatMessage[],
|
||||
index: number,
|
||||
collapsedMessageIds: Set<string>,
|
||||
): boolean {
|
||||
if (messages.length < 2) {
|
||||
return false;
|
||||
}
|
||||
@@ -53,14 +53,24 @@ function shouldCollapseMessages(messages: ChatMessage[], index: number): boolean
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxTimestampDelta = 1000 * 60 * 2; // 2 minutes
|
||||
const maxTimestampDelta = 1000 * 60; // 1 minute
|
||||
const lastTimestamp = new Date(lastMessage?.timestamp).getTime();
|
||||
const thisTimestamp = new Date(message.timestamp).getTime();
|
||||
if (thisTimestamp - lastTimestamp > maxTimestampDelta) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return id === lastMessage?.user.id;
|
||||
if (id !== lastMessage?.user.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Limit the number of messages that can be collapsed in a row.
|
||||
const maxCollapsedMessageCount = 5;
|
||||
if (collapsedMessageIds.size >= maxCollapsedMessageCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkIsModerator(message: ChatMessage | ConnectedClientInfoEvent) {
|
||||
@@ -83,28 +93,33 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
showInput,
|
||||
height,
|
||||
}) => {
|
||||
const [atBottom, setAtBottom] = useState(false);
|
||||
const [showScrollToBottomButton, setShowScrollToBottomButton] = useState(false);
|
||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||
|
||||
const chatContainerRef = useRef(null);
|
||||
const showScrollToBottomButtonDelay = useRef(null);
|
||||
const scrollToBottomDelay = useRef(null);
|
||||
|
||||
const getNameChangeViewForMessage = (message: NameChangeEvent) => {
|
||||
const { oldName, user } = message;
|
||||
const { displayName, displayColor } = user;
|
||||
const color = `var(--theme-color-users-${displayColor})`;
|
||||
const collapsedMessageIds = new Set<string>();
|
||||
|
||||
return (
|
||||
<div className={styles.nameChangeView}>
|
||||
<div style={{ marginRight: 5, height: 'max-content', margin: 'auto 5px auto 0' }}>
|
||||
<EditFilled />
|
||||
</div>
|
||||
<div className={styles.nameChangeText}>
|
||||
<span style={{ color }}>{oldName}</span>
|
||||
<span className={styles.plain}> is now known as </span>
|
||||
<span style={{ color }}>{displayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const setShowScrolltoBottomButtonWithDelay = (show: boolean) => {
|
||||
showScrollToBottomButtonDelay.current = setTimeout(() => {
|
||||
setShowScrollToBottomButton(show);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
// Clear the timer when the component unmounts
|
||||
() => {
|
||||
clearTimeout(showScrollToBottomButtonDelay.current);
|
||||
clearTimeout(scrollToBottomDelay.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getFediverseMessage = (message: FediverseEvent) => <ChatSocialMessage message={message} />;
|
||||
|
||||
const getUserJoinedMessage = (message: ChatMessage) => {
|
||||
const {
|
||||
user: { displayName, displayColor },
|
||||
@@ -123,6 +138,7 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
const { body } = message;
|
||||
return <ChatActionMessage body={body} />;
|
||||
};
|
||||
|
||||
const getConnectedInfoMessage = (message: ConnectedClientInfoEvent) => {
|
||||
const modStatusUpdate = checkIsModerator(message);
|
||||
if (!modStatusUpdate) {
|
||||
@@ -136,28 +152,39 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
return <ChatModeratorNotification />;
|
||||
};
|
||||
|
||||
const getUserChatMessageView = (index: number, message: ChatMessage) => {
|
||||
const collapsed = shouldCollapseMessages(messages, index, collapsedMessageIds);
|
||||
if (!collapsed) {
|
||||
collapsedMessageIds.clear();
|
||||
} else {
|
||||
collapsedMessageIds.add(message.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatUserMessage
|
||||
message={message}
|
||||
showModeratorMenu={isModerator} // Moderators have access to an additional menu
|
||||
highlightString={usernameToHighlight} // What to highlight in the message
|
||||
sentBySelf={message.user?.id === chatUserId} // The local user sent this message
|
||||
sameUserAsLast={collapsed}
|
||||
isAuthorModerator={message.user?.scopes?.includes('MODERATOR')}
|
||||
isAuthorBot={message.user?.scopes?.includes('BOT')}
|
||||
isAuthorAuthenticated={message.user?.authenticated}
|
||||
key={message.id}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const getViewForMessage = (
|
||||
index: number,
|
||||
message: ChatMessage | NameChangeEvent | ConnectedClientInfoEvent,
|
||||
message: ChatMessage | NameChangeEvent | ConnectedClientInfoEvent | FediverseEvent,
|
||||
) => {
|
||||
switch (message.type) {
|
||||
case MessageType.CHAT:
|
||||
return (
|
||||
<ChatUserMessage
|
||||
message={message as ChatMessage}
|
||||
showModeratorMenu={isModerator} // Moderators have access to an additional menu
|
||||
highlightString={usernameToHighlight} // What to highlight in the message
|
||||
sentBySelf={message.user?.id === chatUserId} // The local user sent this message
|
||||
sameUserAsLast={shouldCollapseMessages(messages, index)}
|
||||
isAuthorModerator={(message as ChatMessage).user.scopes?.includes('MODERATOR')}
|
||||
isAuthorAuthenticated={message.user?.authenticated}
|
||||
key={message.id}
|
||||
/>
|
||||
);
|
||||
return getUserChatMessageView(index, message as ChatMessage);
|
||||
case MessageType.NAME_CHANGE:
|
||||
return getNameChangeViewForMessage(message as NameChangeEvent);
|
||||
return <ChatNameChangeMessage message={message as NameChangeEvent} />;
|
||||
case MessageType.CONNECTED_USER_INFO:
|
||||
return getConnectedInfoMessage(message);
|
||||
return getConnectedInfoMessage(message as ConnectedClientInfoEvent);
|
||||
case MessageType.USER_JOINED:
|
||||
return getUserJoinedMessage(message as ChatMessage);
|
||||
case MessageType.CHAT_ACTION:
|
||||
@@ -170,22 +197,29 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
key={message.id}
|
||||
/>
|
||||
);
|
||||
case MessageType.FEDIVERSE_ENGAGEMENT_FOLLOW:
|
||||
return getFediverseMessage(message as FediverseEvent);
|
||||
case MessageType.FEDIVERSE_ENGAGEMENT_LIKE:
|
||||
return getFediverseMessage(message as FediverseEvent);
|
||||
case MessageType.FEDIVERSE_ENGAGEMENT_REPOST:
|
||||
return getFediverseMessage(message as FediverseEvent);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const scrollChatToBottom = (ref, behavior = 'smooth') => {
|
||||
setTimeout(() => {
|
||||
clearTimeout(scrollToBottomDelay.current);
|
||||
clearTimeout(showScrollToBottomButtonDelay.current);
|
||||
scrollToBottomDelay.current = setTimeout(() => {
|
||||
ref.current?.scrollToIndex({
|
||||
index: messages.length - 1,
|
||||
behavior,
|
||||
});
|
||||
setIsAtBottom(true);
|
||||
setShowScrollToBottomButton(false);
|
||||
}, 100);
|
||||
|
||||
setAtBottom(true);
|
||||
};
|
||||
|
||||
// This is a hack to force a scroll to the very bottom of the chat messages
|
||||
@@ -194,6 +228,7 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
scrollChatToBottom(chatContainerRef, 'auto');
|
||||
setShowScrolltoBottomButtonWithDelay(false);
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
@@ -207,22 +242,41 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
ref={chatContainerRef}
|
||||
data={messages}
|
||||
itemContent={(index, message) => getViewForMessage(index, message)}
|
||||
followOutput={(isAtBottom: boolean) => {
|
||||
initialTopMostItemIndex={messages.length - 1}
|
||||
followOutput={() => {
|
||||
clearTimeout(showScrollToBottomButtonDelay.current);
|
||||
|
||||
if (isAtBottom) {
|
||||
scrollChatToBottom(chatContainerRef, 'smooth');
|
||||
setShowScrollToBottomButton(false);
|
||||
scrollChatToBottom(chatContainerRef, 'auto');
|
||||
return 'smooth';
|
||||
}
|
||||
setShowScrolltoBottomButtonWithDelay(true);
|
||||
|
||||
return false;
|
||||
}}
|
||||
alignToBottom
|
||||
atBottomThreshold={70}
|
||||
atBottomStateChange={bottom => {
|
||||
setAtBottom(bottom);
|
||||
setIsAtBottom(bottom);
|
||||
|
||||
if (bottom) {
|
||||
setShowScrollToBottomButton(false);
|
||||
} else {
|
||||
setShowScrolltoBottomButtonWithDelay(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{!atBottom && <ScrollToBotBtn chatContainerRef={chatContainerRef} messages={messages} />}
|
||||
{showScrollToBottomButton && (
|
||||
<ScrollToBotBtn
|
||||
onClick={() => {
|
||||
scrollChatToBottom(chatContainerRef, 'auto');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[messages, usernameToHighlight, chatUserId, isModerator, atBottom],
|
||||
[messages, usernameToHighlight, chatUserId, isModerator, showScrollToBottomButton, isAtBottom],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Button } from 'antd';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { FC, MutableRefObject } from 'react';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import { FC } from 'react';
|
||||
import styles from './ChatContainer.module.scss';
|
||||
|
||||
// Lazy loaded components
|
||||
@@ -12,23 +11,18 @@ const VerticalAlignBottomOutlined = dynamic(
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
type Props = {
|
||||
chatContainerRef: MutableRefObject<any>;
|
||||
messages: ChatMessage[];
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const ScrollToBotBtn: FC<Props> = ({ chatContainerRef, messages }) => (
|
||||
export const ScrollToBotBtn: FC<Props> = ({ onClick }) => (
|
||||
<div className={styles.toBottomWrap}>
|
||||
<Button
|
||||
type="default"
|
||||
style={{ color: 'currentColor' }}
|
||||
icon={<VerticalAlignBottomOutlined />}
|
||||
onClick={() =>
|
||||
chatContainerRef.current.scrollToIndex({
|
||||
index: messages.length - 1,
|
||||
behavior: 'auto',
|
||||
})
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
Go to last message
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.nameChangeView {
|
||||
display: flex;
|
||||
font-size: 0.9rem;
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
padding: 5px 15px;
|
||||
color: var(--theme-color-components-chat-text);
|
||||
& .nameChangeText {
|
||||
font-weight: bold;
|
||||
font-family: var(--theme-text-display-font-family);
|
||||
& .plain {
|
||||
font-weight: normal;
|
||||
font-family: var(--theme-text-body-font-family) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { ChatNameChangeMessage } from './ChatNameChangeMessage';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Chat/Messages/Chat name change',
|
||||
component: ChatNameChangeMessage,
|
||||
} as ComponentMeta<typeof ChatNameChangeMessage>;
|
||||
|
||||
const Template: ComponentStory<typeof ChatNameChangeMessage> = args => (
|
||||
<ChatNameChangeMessage {...args} />
|
||||
);
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
message: {
|
||||
oldName: 'JohnnyOldName',
|
||||
user: {
|
||||
displayName: 'JohnnyNewName',
|
||||
displayColor: '3',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
// export const ChatSocialMessage: FC<ChatSocialMessageProps> = ({ message }) => {
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { FC } from 'react';
|
||||
import { NameChangeEvent } from '../../../interfaces/socket-events';
|
||||
import styles from './ChatNameChangeMessage.module.scss';
|
||||
|
||||
export interface ChatNameChangeMessageProps {
|
||||
message: NameChangeEvent;
|
||||
}
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
const EditFilled = dynamic(() => import('@ant-design/icons/EditFilled'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const ChatNameChangeMessage: FC<ChatNameChangeMessageProps> = ({ message }) => {
|
||||
const { oldName, user } = message;
|
||||
const { displayName, displayColor } = user;
|
||||
const color = `var(--theme-color-users-${displayColor})`;
|
||||
|
||||
return (
|
||||
<div className={styles.nameChangeView}>
|
||||
<div style={{ marginRight: 5, height: 'max-content', margin: 'auto 5px auto 0' }}>
|
||||
<EditFilled />
|
||||
</div>
|
||||
<div className={styles.nameChangeText}>
|
||||
<span style={{ color }}>{oldName}</span>
|
||||
<span className={styles.plain}> is now known as </span>
|
||||
<span style={{ color }}>{displayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,9 +4,7 @@
|
||||
border-style: solid;
|
||||
padding: 10px 10px;
|
||||
border-radius: 15px;
|
||||
height: 85px;
|
||||
width: 300px;
|
||||
overflow: hidden;
|
||||
background-color: var(--theme-color-background-main);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-text-link);
|
||||
@@ -18,11 +16,22 @@
|
||||
border-color: rgba(0, 0, 0, 0.3);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.avatarColumn {
|
||||
max-width: 75px;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.body {
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2rem;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.account {
|
||||
@@ -32,21 +41,16 @@
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
top: -20px;
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
border-color: white;
|
||||
border-width: 1px;
|
||||
border-color: var(--theme-color-background-main);
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border-radius: 50%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ const Template: ComponentStory<typeof ChatSocialMessage> = args => <ChatSocialMe
|
||||
export const Follow = Template.bind({});
|
||||
Follow.args = {
|
||||
message: {
|
||||
type: 'follow',
|
||||
body: 'james followed this live stream.',
|
||||
type: 'FEDIVERSE_ENGAGEMENT_FOLLOW',
|
||||
body: '<p>james followed this live stream.</p>',
|
||||
title: 'james@mastodon.social',
|
||||
image: 'https://mastodon.social/avatars/original/missing.png',
|
||||
link: 'https://mastodon.social/@james',
|
||||
@@ -24,8 +24,8 @@ Follow.args = {
|
||||
export const Like = Template.bind({});
|
||||
Like.args = {
|
||||
message: {
|
||||
type: 'like',
|
||||
body: 'james liked that this stream went live.',
|
||||
type: 'FEDIVERSE_ENGAGEMENT_LIKE',
|
||||
body: '<p>james liked that this stream went live.</p>',
|
||||
title: 'james@mastodon.social',
|
||||
image: 'https://mastodon.social/avatars/original/missing.png',
|
||||
link: 'https://mastodon.social/@james',
|
||||
@@ -35,10 +35,32 @@ Like.args = {
|
||||
export const Repost = Template.bind({});
|
||||
Repost.args = {
|
||||
message: {
|
||||
type: 'repost',
|
||||
body: 'james shared this stream with their followers.',
|
||||
type: 'FEDIVERSE_ENGAGEMENT_REPOST',
|
||||
body: '<p>james shared this stream with their followers.</p>',
|
||||
title: 'james@mastodon.social',
|
||||
image: 'https://mastodon.social/avatars/original/missing.png',
|
||||
link: 'https://mastodon.social/@james',
|
||||
},
|
||||
};
|
||||
|
||||
export const LongAccountName = Template.bind({});
|
||||
LongAccountName.args = {
|
||||
message: {
|
||||
type: 'FEDIVERSE_ENGAGEMENT_REPOST',
|
||||
body: '<p>james shared this stream with their followers.</p>',
|
||||
title: 'littlejimmywilliams@technology.biz.net.org.technology.gov',
|
||||
image: 'https://mastodon.social/avatars/original/missing.png',
|
||||
link: 'https://mastodon.social/@james',
|
||||
},
|
||||
};
|
||||
|
||||
export const InvalidAvatarImage = Template.bind({});
|
||||
InvalidAvatarImage.args = {
|
||||
message: {
|
||||
type: 'FEDIVERSE_ENGAGEMENT_REPOST',
|
||||
body: '<p>james shared this stream with their followers.</p>',
|
||||
title: 'james@mastodon.social',
|
||||
image: 'https://xx.xx/avatars/original/missing.png',
|
||||
link: 'https://mastodon.social/@james',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,13 +25,13 @@ export const ChatSocialMessage: FC<ChatSocialMessageProps> = ({ message }) => {
|
||||
let Icon;
|
||||
|
||||
switch (type.toString()) {
|
||||
case 'follow':
|
||||
case 'FEDIVERSE_ENGAGEMENT_FOLLOW':
|
||||
Icon = FollowIcon;
|
||||
break;
|
||||
case 'like':
|
||||
case 'FEDIVERSE_ENGAGEMENT_LIKE':
|
||||
Icon = LikeIcon;
|
||||
break;
|
||||
case 'repost':
|
||||
case 'FEDIVERSE_ENGAGEMENT_REPOST':
|
||||
Icon = RepostIcon;
|
||||
break;
|
||||
default:
|
||||
@@ -42,15 +42,16 @@ export const ChatSocialMessage: FC<ChatSocialMessageProps> = ({ message }) => {
|
||||
<div className={cn([styles.follower, 'chat-message_social'])}>
|
||||
<a href={link} target="_blank" rel="noreferrer">
|
||||
<Row wrap={false}>
|
||||
<Col span={6}>
|
||||
<Avatar src={image} alt="Avatar" className={styles.avatar}>
|
||||
<img src="/logo" alt="Logo" className={styles.placeholder} />
|
||||
<Col span={6} className={styles.avatarColumn}>
|
||||
<Avatar src={image} alt="Avatar" className={styles.avatar} size="large">
|
||||
{title.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
|
||||
<Icon className={styles.icon} />
|
||||
</Col>
|
||||
<Col>
|
||||
<Row className={styles.account}>{title}</Row>
|
||||
<Row className={styles.body}>{body}</Row>
|
||||
<Row className={styles.body} dangerouslySetInnerHTML={{ __html: body }} />
|
||||
</Col>
|
||||
</Row>
|
||||
</a>
|
||||
|
||||
@@ -30,9 +30,11 @@
|
||||
}
|
||||
|
||||
mark {
|
||||
padding-left: 0.35em;
|
||||
padding-right: 0.35em;
|
||||
background-color: var(--theme-color-palette-12);
|
||||
padding-left: 0.3em;
|
||||
padding-right: 0.3em;
|
||||
color: var(--theme-color-palette-4);
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
background-color: var(--color-owncast-palette-7);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import { Highlight } from 'react-highlighter-ts';
|
||||
import { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Interweave } from 'interweave';
|
||||
import { UrlMatcher } from 'interweave-autolink';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import styles from './ChatSystemMessage.module.scss';
|
||||
import { ChatMessageHighlightMatcher } from '../ChatUserMessage/customMatcher';
|
||||
|
||||
export type ChatSystemMessageProps = {
|
||||
message: ChatMessage;
|
||||
@@ -21,8 +22,13 @@ export const ChatSystemMessage: FC<ChatSystemMessageProps> = ({
|
||||
<div className={styles.user}>
|
||||
<span className={styles.userName}>{displayName}</span>
|
||||
</div>
|
||||
<Highlight search={highlightString}>
|
||||
<div className={styles.message} dangerouslySetInnerHTML={{ __html: body }} />
|
||||
</Highlight>
|
||||
<Interweave
|
||||
className={styles.message}
|
||||
content={body}
|
||||
matchers={[
|
||||
new UrlMatcher('url', { validateTLD: false }),
|
||||
new ChatMessageHighlightMatcher('highlight', { highlightString }),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ export default {
|
||||
component: ChatTextField,
|
||||
parameters: {
|
||||
fetchMock: mocks,
|
||||
chromatic: { diffThreshold: 0.8 },
|
||||
chromatic: { diffThreshold: 0.88 },
|
||||
|
||||
design: {
|
||||
type: 'image',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Popover } from 'antd';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Transforms, createEditor, BaseEditor, Text, Descendant, Editor, Node, Path } from 'slate';
|
||||
import { Transforms, createEditor, BaseEditor, Text, Descendant, Editor } from 'slate';
|
||||
import { Slate, Editable, withReact, ReactEditor, useSelected, useFocused } from 'slate-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import classNames from 'classnames';
|
||||
@@ -169,31 +169,10 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText }) => {
|
||||
const insertImage = (url, name) => {
|
||||
if (!url) return;
|
||||
|
||||
const { selection } = editor;
|
||||
const image = createImageNode(name, url, name);
|
||||
|
||||
Transforms.insertNodes(editor, image, { select: true });
|
||||
|
||||
if (selection) {
|
||||
const [parentNode, parentPath] = Editor.parent(editor, selection.focus?.path);
|
||||
|
||||
if (editor.isVoid(parentNode) || Node.string(parentNode).length) {
|
||||
// Insert the new image node after the void node or a node with content
|
||||
Transforms.insertNodes(editor, image, {
|
||||
at: Path.next(parentPath),
|
||||
select: true,
|
||||
});
|
||||
} else {
|
||||
// If the node is empty, replace it instead
|
||||
// Transforms.removeNodes(editor, { at: parentPath });
|
||||
Transforms.insertNodes(editor, image, { at: parentPath, select: true });
|
||||
Editor.normalize(editor, { force: true });
|
||||
}
|
||||
} else {
|
||||
// Insert the new image node at the bottom of the Editor when selection
|
||||
// is falsey
|
||||
Transforms.insertNodes(editor, image, { select: true });
|
||||
}
|
||||
Transforms.insertNodes(editor, image);
|
||||
Editor.normalize(editor, { force: true });
|
||||
};
|
||||
|
||||
// Native emoji
|
||||
|
||||
17
web/components/chat/ChatUserBadge/BotUserBadge.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { FC } from 'react';
|
||||
import { ChatUserBadge } from './ChatUserBadge';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
const BulbFilled = dynamic(() => import('@ant-design/icons/BulbFilled'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export type BotBadgeProps = {
|
||||
userColor: number;
|
||||
};
|
||||
|
||||
export const BotUserBadge: FC<BotBadgeProps> = ({ userColor }) => (
|
||||
<ChatUserBadge badge={<BulbFilled />} userColor={userColor} title="Bot" />
|
||||
);
|
||||
@@ -3,6 +3,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { ChatUserBadge } from './ChatUserBadge';
|
||||
import { ModerationBadge } from './ModerationBadge';
|
||||
import { AuthedUserBadge } from './AuthedUserBadge';
|
||||
import { BotUserBadge } from './BotUserBadge';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Chat/Messages/User Flag',
|
||||
@@ -24,6 +25,8 @@ const AuthedTemplate: ComponentStory<typeof ModerationBadge> = args => (
|
||||
<AuthedUserBadge {...args} />
|
||||
);
|
||||
|
||||
const BotTemplate: ComponentStory<typeof BotUserBadge> = args => <BotUserBadge {...args} />;
|
||||
|
||||
export const Authenticated = AuthedTemplate.bind({});
|
||||
Authenticated.args = {
|
||||
userColor: '3',
|
||||
@@ -34,6 +37,11 @@ Moderator.args = {
|
||||
userColor: '5',
|
||||
};
|
||||
|
||||
export const Bot = BotTemplate.bind({});
|
||||
Bot.args = {
|
||||
userColor: '7',
|
||||
};
|
||||
|
||||
export const Generic = Template.bind({});
|
||||
Generic.args = {
|
||||
badge: '?',
|
||||
|
||||
@@ -56,7 +56,7 @@ const messageWithLinkAndCustomEmoji: ChatMessage = JSON.parse(`{
|
||||
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
|
||||
"scopes": []
|
||||
},
|
||||
"body": "Test message with a link https://owncast.online and a custom emoji <img src='/img/emoji/blob/ablobattention.gif' width='30px'/> ."}`);
|
||||
"body": "Test message with a link https://owncast.online and a custom emoji <img src='/img/emoji/mutant/skull.svg' width='30px'/> ."}`);
|
||||
|
||||
const moderatorMessage: ChatMessage = JSON.parse(`{
|
||||
"type": "CHAT",
|
||||
@@ -89,6 +89,22 @@ const authenticatedUserMessage: ChatMessage = JSON.parse(`{
|
||||
},
|
||||
"body": "I am an authenticated user."}`);
|
||||
|
||||
const botUserMessage: ChatMessage = JSON.parse(`{
|
||||
"type": "CHAT",
|
||||
"id": "wY-MEXwnR",
|
||||
"timestamp": "2022-04-28T20:30:27.001762726Z",
|
||||
"user": {
|
||||
"id": "h_5GQ6E7R",
|
||||
"displayName": "EliteMooseTaskForce",
|
||||
"displayColor": 7,
|
||||
"createdAt": "2022-03-24T03:52:37.966584694Z",
|
||||
"previousNames": ["gifted-nobel", "EliteMooseTaskForce"],
|
||||
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
|
||||
"authenticated": true,
|
||||
"scopes": ["bot"]
|
||||
},
|
||||
"body": "I am a bot."}`);
|
||||
|
||||
export const WithoutModeratorMenu = Template.bind({});
|
||||
WithoutModeratorMenu.args = {
|
||||
message: standardMessage,
|
||||
@@ -121,6 +137,13 @@ FromAuthenticatedUser.args = {
|
||||
isAuthorAuthenticated: true,
|
||||
};
|
||||
|
||||
export const FromBotUser = Template.bind({});
|
||||
FromBotUser.args = {
|
||||
message: botUserMessage,
|
||||
showModeratorMenu: false,
|
||||
isAuthorBot: true,
|
||||
};
|
||||
|
||||
export const WithStringHighlighted = Template.bind({});
|
||||
WithStringHighlighted.args = {
|
||||
message: standardMessage,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import { FC, ReactNode } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Tooltip } from 'antd';
|
||||
@@ -14,6 +13,7 @@ import { accessTokenAtom } from '../../stores/ClientConfigStore';
|
||||
import { User } from '../../../interfaces/user.model';
|
||||
import { AuthedUserBadge } from '../ChatUserBadge/AuthedUserBadge';
|
||||
import { ModerationBadge } from '../ChatUserBadge/ModerationBadge';
|
||||
import { BotUserBadge } from '../ChatUserBadge/BotUserBadge';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
@@ -35,6 +35,7 @@ export type ChatUserMessageProps = {
|
||||
sameUserAsLast: boolean;
|
||||
isAuthorModerator: boolean;
|
||||
isAuthorAuthenticated: boolean;
|
||||
isAuthorBot: boolean;
|
||||
};
|
||||
|
||||
export type UserTooltipProps = {
|
||||
@@ -61,6 +62,7 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
|
||||
sameUserAsLast,
|
||||
isAuthorModerator,
|
||||
isAuthorAuthenticated,
|
||||
isAuthorBot,
|
||||
}) => {
|
||||
const { id: messageId, body, user, timestamp } = message;
|
||||
const { id: userId, displayName, displayColor } = user;
|
||||
@@ -76,7 +78,9 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
|
||||
if (isAuthorAuthenticated) {
|
||||
badgeNodes.push(<AuthedUserBadge key="auth" userColor={displayColor} />);
|
||||
}
|
||||
|
||||
if (isAuthorBot) {
|
||||
badgeNodes.push(<BotUserBadge key="bot" userColor={displayColor} />);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
font-size: 1.7rem;
|
||||
font-weight: bold;
|
||||
line-height: 30px;
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -48,6 +49,7 @@
|
||||
line-height: 1.3;
|
||||
color: var(--theme-color-background-header);
|
||||
max-width: 900px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,21 +8,13 @@ import styles from './ContentHeader.module.scss';
|
||||
|
||||
export type ContentHeaderProps = {
|
||||
name: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
links: SocialLink[];
|
||||
logo: string;
|
||||
};
|
||||
|
||||
export const ContentHeader: FC<ContentHeaderProps> = ({
|
||||
name,
|
||||
title,
|
||||
summary,
|
||||
logo,
|
||||
tags,
|
||||
links,
|
||||
}) => (
|
||||
export const ContentHeader: FC<ContentHeaderProps> = ({ name, summary, logo, tags, links }) => (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.logoTitleSection}>
|
||||
<div className={styles.logo}>
|
||||
@@ -31,7 +23,7 @@ export const ContentHeader: FC<ContentHeaderProps> = ({
|
||||
<div className={styles.titleSection}>
|
||||
<h2 className={cn(styles.title, styles.row, 'header-title')}>{name}</h2>
|
||||
<h3 className={cn(styles.subtitle, styles.row, 'header-subtitle')}>
|
||||
<Linkify>{title || summary}</Linkify>
|
||||
<Linkify>{summary}</Linkify>
|
||||
</h3>
|
||||
<div className={cn(styles.tagList, styles.row)}>
|
||||
{tags.length > 0 && tags.map(tag => <span key={tag}>#{tag} </span>)}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
export const HtmlComment = ({ text }) => (
|
||||
<span style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `\n\n<!-- ${text} -->` }} />
|
||||
);
|
||||
158
web/components/layouts/Main/Main.stories.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { MutableSnapshot, RecoilRoot } from 'recoil';
|
||||
import { makeEmptyClientConfig } from '../../../interfaces/client-config.model';
|
||||
import { ServerStatus, makeEmptyServerStatus } from '../../../interfaces/server-status.model';
|
||||
import {
|
||||
accessTokenAtom,
|
||||
appStateAtom,
|
||||
chatMessagesAtom,
|
||||
chatVisibleToggleAtom,
|
||||
clientConfigStateAtom,
|
||||
currentUserAtom,
|
||||
fatalErrorStateAtom,
|
||||
isMobileAtom,
|
||||
isVideoPlayingAtom,
|
||||
serverStatusState,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
import { Main } from './Main';
|
||||
import { ClientConfigServiceContext } from '../../../services/client-config-service';
|
||||
import { ChatServiceContext } from '../../../services/chat-service';
|
||||
import {
|
||||
ServerStatusServiceContext,
|
||||
ServerStatusStaticService,
|
||||
} from '../../../services/status-service';
|
||||
import { clientConfigServiceMockOf } from '../../../services/client-config-service.mock';
|
||||
import chatServiceMockOf from '../../../services/chat-service.mock';
|
||||
import serverStatusServiceMockOf from '../../../services/status-service.mock';
|
||||
import { VideoSettingsServiceContext } from '../../../services/video-settings-service';
|
||||
import videoSettingsServiceMockOf from '../../../services/video-settings-service.mock';
|
||||
import { grootUser, spidermanUser } from '../../../interfaces/user.fixture';
|
||||
import { exampleChatHistory } from '../../../interfaces/chat-message.fixture';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Layout/Main',
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} satisfies ComponentMeta<typeof Main>;
|
||||
|
||||
type StateInitializer = (mutableState: MutableSnapshot) => void;
|
||||
|
||||
const composeStateInitializers =
|
||||
(...fns: Array<StateInitializer>): StateInitializer =>
|
||||
state =>
|
||||
fns.forEach(fn => fn?.(state));
|
||||
|
||||
const defaultClientConfig = {
|
||||
...makeEmptyClientConfig(),
|
||||
logo: 'http://localhost:8080/logo',
|
||||
name: "Spiderman's super serious stream",
|
||||
summary: 'Strong Spidey stops supervillains! Streamed Saturdays & Sundays.',
|
||||
extraPageContent: 'Spiderman is **cool**',
|
||||
};
|
||||
|
||||
const defaultServerStatus = makeEmptyServerStatus();
|
||||
const onlineServerStatus: ServerStatus = {
|
||||
...defaultServerStatus,
|
||||
online: true,
|
||||
viewerCount: 5,
|
||||
};
|
||||
|
||||
const initializeDefaultState = (mutableState: MutableSnapshot) => {
|
||||
mutableState.set(appStateAtom, {
|
||||
videoAvailable: false,
|
||||
chatAvailable: false,
|
||||
});
|
||||
mutableState.set(clientConfigStateAtom, defaultClientConfig);
|
||||
mutableState.set(chatVisibleToggleAtom, true);
|
||||
mutableState.set(accessTokenAtom, 'token');
|
||||
mutableState.set(currentUserAtom, {
|
||||
...spidermanUser,
|
||||
isModerator: false,
|
||||
});
|
||||
mutableState.set(serverStatusState, defaultServerStatus);
|
||||
mutableState.set(isMobileAtom, false);
|
||||
|
||||
mutableState.set(chatMessagesAtom, exampleChatHistory);
|
||||
mutableState.set(isVideoPlayingAtom, false);
|
||||
mutableState.set(fatalErrorStateAtom, null);
|
||||
};
|
||||
|
||||
const ClientConfigServiceMock = clientConfigServiceMockOf(defaultClientConfig);
|
||||
const ChatServiceMock = chatServiceMockOf(exampleChatHistory, {
|
||||
...grootUser,
|
||||
accessToken: 'some fake token',
|
||||
});
|
||||
const DefaultServerStatusServiceMock = serverStatusServiceMockOf(defaultServerStatus);
|
||||
const OnlineServerStatusServiceMock = serverStatusServiceMockOf(onlineServerStatus);
|
||||
const VideoSettingsServiceMock = videoSettingsServiceMockOf([]);
|
||||
|
||||
const Template: ComponentStory<typeof Main> = ({
|
||||
initializeState,
|
||||
ServerStatusServiceMock = DefaultServerStatusServiceMock,
|
||||
...args
|
||||
}: {
|
||||
initializeState: (mutableState: MutableSnapshot) => void;
|
||||
ServerStatusServiceMock: ServerStatusStaticService;
|
||||
}) => (
|
||||
<RecoilRoot initializeState={composeStateInitializers(initializeDefaultState, initializeState)}>
|
||||
<ClientConfigServiceContext.Provider value={ClientConfigServiceMock}>
|
||||
<ChatServiceContext.Provider value={ChatServiceMock}>
|
||||
<ServerStatusServiceContext.Provider value={ServerStatusServiceMock}>
|
||||
<VideoSettingsServiceContext.Provider value={VideoSettingsServiceMock}>
|
||||
<Main {...args} />
|
||||
</VideoSettingsServiceContext.Provider>
|
||||
</ServerStatusServiceContext.Provider>
|
||||
</ChatServiceContext.Provider>
|
||||
</ClientConfigServiceContext.Provider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
export const OfflineDesktop: typeof Template = Template.bind({});
|
||||
|
||||
export const OfflineMobile: typeof Template = Template.bind({});
|
||||
OfflineMobile.args = {
|
||||
initializeState: (mutableState: MutableSnapshot) => {
|
||||
mutableState.set(isMobileAtom, true);
|
||||
},
|
||||
};
|
||||
OfflineMobile.parameters = {
|
||||
viewport: {
|
||||
defaultViewport: 'mobile1',
|
||||
},
|
||||
};
|
||||
|
||||
export const OfflineTablet: typeof Template = Template.bind({});
|
||||
OfflineTablet.parameters = {
|
||||
viewport: {
|
||||
defaultViewport: 'tablet',
|
||||
},
|
||||
};
|
||||
|
||||
export const Online: typeof Template = Template.bind({});
|
||||
Online.args = {
|
||||
ServerStatusServiceMock: OnlineServerStatusServiceMock,
|
||||
};
|
||||
|
||||
export const OnlineMobile: typeof Template = Online.bind({});
|
||||
OnlineMobile.args = {
|
||||
ServerStatusServiceMock: OnlineServerStatusServiceMock,
|
||||
initializeState: (mutableState: MutableSnapshot) => {
|
||||
mutableState.set(isMobileAtom, true);
|
||||
},
|
||||
};
|
||||
OnlineMobile.parameters = {
|
||||
viewport: {
|
||||
defaultViewport: 'mobile1',
|
||||
},
|
||||
};
|
||||
|
||||
export const OnlineTablet: typeof Template = Online.bind({});
|
||||
OnlineTablet.args = {
|
||||
ServerStatusServiceMock: OnlineServerStatusServiceMock,
|
||||
};
|
||||
OnlineTablet.parameters = {
|
||||
viewport: {
|
||||
defaultViewport: 'tablet',
|
||||
},
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
clientConfigStateAtom,
|
||||
fatalErrorStateAtom,
|
||||
appStateAtom,
|
||||
serverStatusState,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
import { Content } from '../../ui/Content/Content';
|
||||
import { Header } from '../../ui/Header/Header';
|
||||
@@ -25,6 +26,7 @@ import styles from './Main.module.scss';
|
||||
import { PushNotificationServiceWorker } from '../../workers/PushNotificationServiceWorker/PushNotificationServiceWorker';
|
||||
import { AppStateOptions } from '../../stores/application-state';
|
||||
import { Noscript } from '../../ui/Noscript/Noscript';
|
||||
import { ServerStatus } from '../../../interfaces/server-status.model';
|
||||
|
||||
const lockBodyStyle = `
|
||||
body {
|
||||
@@ -46,7 +48,8 @@ const FatalErrorStateModal = dynamic(
|
||||
|
||||
export const Main: FC = () => {
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const { name, title, customStyles } = clientConfig;
|
||||
const clientStatus = useRecoilValue<ServerStatus>(serverStatusState);
|
||||
const { name, customStyles } = clientConfig;
|
||||
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
|
||||
const fatalError = useRecoilValue<DisplayableError>(fatalErrorStateAtom);
|
||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||
@@ -54,12 +57,14 @@ export const Main: FC = () => {
|
||||
const layoutRef = useRef<HTMLDivElement>(null);
|
||||
const { chatDisabled } = clientConfig;
|
||||
const { videoAvailable } = appState;
|
||||
const { online, streamTitle } = clientStatus;
|
||||
|
||||
useEffect(() => {
|
||||
setupNoLinkReferrer(layoutRef.current);
|
||||
}, []);
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const headerText = online ? streamTitle || name : name;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -143,7 +148,7 @@ export const Main: FC = () => {
|
||||
|
||||
<Layout ref={layoutRef} className={styles.layout}>
|
||||
<Header
|
||||
name={title || name}
|
||||
name={headerText}
|
||||
chatAvailable={isChatAvailable}
|
||||
chatDisabled={chatDisabled}
|
||||
online={videoAvailable}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, useContext, useEffect, useState } from 'react';
|
||||
import { atom, selector, useRecoilState, useSetRecoilState, RecoilEnv } from 'recoil';
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model';
|
||||
import ClientConfigService from '../../services/client-config-service';
|
||||
import ChatService from '../../services/chat-service';
|
||||
import { ClientConfigServiceContext } from '../../services/client-config-service';
|
||||
import { ChatServiceContext } from '../../services/chat-service';
|
||||
import WebsocketService from '../../services/websocket-service';
|
||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||
import { CurrentUser } from '../../interfaces/current-user';
|
||||
@@ -20,10 +20,11 @@ import {
|
||||
ChatEvent,
|
||||
MessageVisibilityEvent,
|
||||
SocketEvent,
|
||||
FediverseEvent,
|
||||
} from '../../interfaces/socket-events';
|
||||
import { mergeMeta } from '../../utils/helpers';
|
||||
import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler';
|
||||
import ServerStatusService from '../../services/status-service';
|
||||
import { ServerStatusServiceContext } from '../../services/status-service';
|
||||
import handleNameChangeEvent from './eventhandlers/handleNameChangeEvent';
|
||||
import { DisplayableError } from '../../types/displayable-error';
|
||||
|
||||
@@ -154,13 +155,17 @@ export const visibleChatMessagesSelector = selector<ChatMessage[]>({
|
||||
});
|
||||
|
||||
export const ClientConfigStore: FC = () => {
|
||||
const ClientConfigService = useContext(ClientConfigServiceContext);
|
||||
const ChatService = useContext(ChatServiceContext);
|
||||
const ServerStatusService = useContext(ServerStatusServiceContext);
|
||||
|
||||
const [appState, appStateSend, appStateService] = useMachine(appStateModel);
|
||||
const [currentUser, setCurrentUser] = useRecoilState(currentUserAtom);
|
||||
const setChatAuthenticated = useSetRecoilState<boolean>(chatAuthenticatedAtom);
|
||||
const [clientConfig, setClientConfig] = useRecoilState<ClientConfig>(clientConfigStateAtom);
|
||||
const [, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState);
|
||||
const setClockSkew = useSetRecoilState<Number>(clockSkewAtom);
|
||||
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
||||
const [chatMessages, setChatMessages] = useRecoilState<SocketEvent[]>(chatMessagesAtom);
|
||||
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
||||
const setAppState = useSetRecoilState<AppStateOptions>(appStateAtom);
|
||||
const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom);
|
||||
@@ -208,7 +213,7 @@ export const ClientConfigStore: FC = () => {
|
||||
setHasLoadedConfig(true);
|
||||
} catch (error) {
|
||||
setGlobalFatalError('Unable to reach Owncast server', serverConnectivityError);
|
||||
console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
|
||||
console.error(`ClientConfigService -> getConfig() ERROR: \n`, error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,7 +232,7 @@ export const ClientConfigStore: FC = () => {
|
||||
} catch (error) {
|
||||
sendEvent([AppStateEvent.Fail]);
|
||||
setGlobalFatalError('Unable to reach Owncast server', serverConnectivityError);
|
||||
console.error(`serverStatusState -> getStatus() ERROR: \n${error}`);
|
||||
console.error(`serverStatusState -> getStatus() ERROR: \n`, error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -307,6 +312,15 @@ export const ClientConfigStore: FC = () => {
|
||||
case MessageType.CHAT_ACTION:
|
||||
setChatMessages(currentState => [...currentState, message as ChatEvent]);
|
||||
break;
|
||||
case MessageType.FEDIVERSE_ENGAGEMENT_FOLLOW:
|
||||
setChatMessages(currentState => [...currentState, message as FediverseEvent]);
|
||||
break;
|
||||
case MessageType.FEDIVERSE_ENGAGEMENT_LIKE:
|
||||
setChatMessages(currentState => [...currentState, message as FediverseEvent]);
|
||||
break;
|
||||
case MessageType.FEDIVERSE_ENGAGEMENT_REPOST:
|
||||
setChatMessages(currentState => [...currentState, message as FediverseEvent]);
|
||||
break;
|
||||
case MessageType.VISIBILITY_UPDATE:
|
||||
handleMessageVisibilityChange(message as MessageVisibilityEvent);
|
||||
break;
|
||||
|
||||
@@ -3,19 +3,46 @@
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--theme-color-background-main);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
||||
@include screen(desktop) {
|
||||
height: var(--content-height);
|
||||
}
|
||||
|
||||
.mainSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-rows: min-content // Skeleton when app is loading
|
||||
minmax(30px, min-content) // player
|
||||
min-content // status bar when live
|
||||
min-content // mid section
|
||||
minmax(250px, 1fr) // mobile content
|
||||
;
|
||||
grid-template-columns: 100%;
|
||||
|
||||
&.offline {
|
||||
grid-template-rows: min-content // Skeleton when app is loading
|
||||
min-content // offline banner
|
||||
min-content // status bar when live
|
||||
min-content // mid section
|
||||
minmax(250px, 1fr) // mobile content
|
||||
;
|
||||
}
|
||||
|
||||
@include screen(tablet) {
|
||||
grid-template-columns: 100vw;
|
||||
}
|
||||
|
||||
@include screen(desktop) {
|
||||
overflow-y: scroll;
|
||||
grid-template-rows: unset;
|
||||
|
||||
&.offline {
|
||||
grid-template-rows: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +54,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topSection {
|
||||
padding: 0;
|
||||
background-color: var(--theme-color-components-video-background);
|
||||
}
|
||||
.lowerSection {
|
||||
padding: 0em 2%;
|
||||
margin-bottom: 2em;
|
||||
@@ -38,12 +61,21 @@
|
||||
|
||||
.lowerSectionMobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
padding: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.topSectionElement {
|
||||
background-color: var(--theme-color-components-video-background);
|
||||
}
|
||||
|
||||
.statusBar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.leftCol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -53,13 +85,6 @@
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
grid-template-rows: 1fr auto;
|
||||
}
|
||||
|
||||
.replacementBar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -101,3 +126,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottomPageContentContainer {
|
||||
background-color: var(--theme-color-components-content-background);
|
||||
padding: calc(2 * var(--content-padding));
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Skeleton } from 'antd';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import classnames from 'classnames';
|
||||
import { LOCAL_STORAGE_KEYS, getLocalStorage, setLocalStorage } from '../../../utils/localStorage';
|
||||
import isPushNotificationSupported from '../../../utils/browserPushNotifications';
|
||||
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
serverStatusState,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
import { ClientConfig } from '../../../interfaces/client-config.model';
|
||||
import { CustomPageContent } from '../CustomPageContent/CustomPageContent';
|
||||
|
||||
import styles from './Content.module.scss';
|
||||
import { Sidebar } from '../Sidebar/Sidebar';
|
||||
@@ -29,30 +29,21 @@ import { OfflineBanner } from '../OfflineBanner/OfflineBanner';
|
||||
import { AppStateOptions } from '../../stores/application-state';
|
||||
import { FollowButton } from '../../action-buttons/FollowButton';
|
||||
import { NotifyButton } from '../../action-buttons/NotifyButton';
|
||||
import { ContentHeader } from '../../common/ContentHeader/ContentHeader';
|
||||
import { ServerStatus } from '../../../interfaces/server-status.model';
|
||||
import { Statusbar } from '../Statusbar/Statusbar';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import { ExternalAction } from '../../../interfaces/external-action';
|
||||
import { Modal } from '../Modal/Modal';
|
||||
import { ActionButtonMenu } from '../../action-buttons/ActionButtonMenu/ActionButtonMenu';
|
||||
import { DesktopContent } from './DesktopContent';
|
||||
import { MobileContent } from './MobileContent';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
const FollowerCollection = dynamic(
|
||||
() =>
|
||||
import('../followers/FollowerCollection/FollowerCollection').then(
|
||||
mod => mod.FollowerCollection,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
const FollowModal = dynamic(
|
||||
() => import('../../modals/FollowModal/FollowModal').then(mod => mod.FollowModal),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <Skeleton loading active paragraph={{ rows: 8 }} />,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -63,6 +54,7 @@ const BrowserNotifyModal = dynamic(
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <Skeleton loading active paragraph={{ rows: 6 }} />,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -70,6 +62,7 @@ const NotifyReminderPopup = dynamic(
|
||||
() => import('../NotifyReminderPopup/NotifyReminderPopup').then(mod => mod.NotifyReminderPopup),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <Skeleton loading active paragraph={{ rows: 8 }} />,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -81,142 +74,8 @@ const OwncastPlayer = dynamic(
|
||||
},
|
||||
);
|
||||
|
||||
const ChatContainer = dynamic(
|
||||
() => import('../../chat/ChatContainer/ChatContainer').then(mod => mod.ChatContainer),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
const Tabs = dynamic(() => import('antd').then(mod => mod.Tabs), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const DesktopContent = ({
|
||||
name,
|
||||
streamTitle,
|
||||
summary,
|
||||
tags,
|
||||
socialHandles,
|
||||
extraPageContent,
|
||||
setShowFollowModal,
|
||||
supportFediverseFeatures,
|
||||
}) => {
|
||||
const aboutTabContent = <CustomPageContent content={extraPageContent} />;
|
||||
const followersTabContent = (
|
||||
<div>
|
||||
<FollowerCollection name={name} onFollowButtonClick={() => setShowFollowModal(true)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const items = [{ label: 'About', key: '2', children: aboutTabContent }];
|
||||
if (supportFediverseFeatures) {
|
||||
items.push({ label: 'Followers', key: '3', children: followersTabContent });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.lowerHalf} id="skip-to-content">
|
||||
<ContentHeader
|
||||
name={name}
|
||||
title={streamTitle}
|
||||
summary={summary}
|
||||
tags={tags}
|
||||
links={socialHandles}
|
||||
logo="/logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.lowerSection}>
|
||||
{items.length > 1 ? <Tabs defaultActiveKey="0" items={items} /> : aboutTabContent}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileContent = ({
|
||||
name,
|
||||
streamTitle,
|
||||
summary,
|
||||
tags,
|
||||
socialHandles,
|
||||
extraPageContent,
|
||||
messages,
|
||||
currentUser,
|
||||
showChat,
|
||||
actions,
|
||||
setExternalActionToDisplay,
|
||||
setShowNotifyPopup,
|
||||
setShowFollowModal,
|
||||
supportFediverseFeatures,
|
||||
supportsBrowserNotifications,
|
||||
}) => {
|
||||
if (!currentUser) {
|
||||
return <Skeleton loading active paragraph={{ rows: 7 }} />;
|
||||
}
|
||||
const { id, displayName } = currentUser;
|
||||
|
||||
const chatContent = showChat && (
|
||||
<ChatContainer
|
||||
messages={messages}
|
||||
usernameToHighlight={displayName}
|
||||
chatUserId={id}
|
||||
isModerator={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const aboutTabContent = (
|
||||
<>
|
||||
<ContentHeader
|
||||
name={name}
|
||||
title={streamTitle}
|
||||
summary={summary}
|
||||
tags={tags}
|
||||
links={socialHandles}
|
||||
logo="/logo"
|
||||
/>
|
||||
<CustomPageContent content={extraPageContent} />
|
||||
</>
|
||||
);
|
||||
const followersTabContent = (
|
||||
<FollowerCollection name={name} onFollowButtonClick={() => setShowFollowModal(true)} />
|
||||
);
|
||||
|
||||
const items = [
|
||||
showChat && { label: 'Chat', key: '0', children: chatContent },
|
||||
{ label: 'About', key: '2', children: aboutTabContent },
|
||||
{ label: 'Followers', key: '3', children: followersTabContent },
|
||||
];
|
||||
|
||||
const replacementTabBar = (props, DefaultTabBar) => (
|
||||
<div className={styles.replacementBar}>
|
||||
<DefaultTabBar {...props} className={styles.defaultTabBar} />
|
||||
<ActionButtonMenu
|
||||
className={styles.actionButtonMenu}
|
||||
showFollowItem={supportFediverseFeatures}
|
||||
showNotifyItem={supportsBrowserNotifications}
|
||||
actions={actions}
|
||||
externalActionSelected={setExternalActionToDisplay}
|
||||
notifyItemSelected={() => setShowNotifyPopup(true)}
|
||||
followItemSelected={() => setShowFollowModal(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.lowerSectionMobile}>
|
||||
<Tabs
|
||||
className={styles.tabs}
|
||||
defaultActiveKey="0"
|
||||
items={items}
|
||||
renderTabBar={replacementTabBar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ExternalModal = ({ externalActionToDisplay, setExternalActionToDisplay }) => {
|
||||
const { title, description, url } = externalActionToDisplay;
|
||||
const { title, description, url, html } = externalActionToDisplay;
|
||||
return (
|
||||
<Modal
|
||||
title={description || title}
|
||||
@@ -224,7 +83,19 @@ const ExternalModal = ({ externalActionToDisplay, setExternalActionToDisplay })
|
||||
open={!!externalActionToDisplay}
|
||||
height="80vh"
|
||||
handleCancel={() => setExternalActionToDisplay(null)}
|
||||
/>
|
||||
>
|
||||
{html ? (
|
||||
<div
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -268,7 +139,8 @@ export const Content: FC = () => {
|
||||
|
||||
const externalActionSelected = (action: ExternalAction) => {
|
||||
const { openExternally, url } = action;
|
||||
if (openExternally) {
|
||||
// apply openExternally only if we don't have an HTML embed
|
||||
if (openExternally && url) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
setExternalActionToDisplay(action);
|
||||
@@ -277,7 +149,7 @@ export const Content: FC = () => {
|
||||
|
||||
const externalActionButtons = externalActions.map(action => (
|
||||
<ActionButton
|
||||
key={action.url}
|
||||
key={action.url || action.html}
|
||||
action={action}
|
||||
externalActionSelected={externalActionSelected}
|
||||
/>
|
||||
@@ -331,105 +203,111 @@ export const Content: FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.main}>
|
||||
<div className={styles.root}>
|
||||
<div className={styles.mainSection}>
|
||||
<div className={styles.topSection}>
|
||||
{appState.appLoading && <Skeleton loading active paragraph={{ rows: 7 }} />}
|
||||
{online && (
|
||||
<OwncastPlayer
|
||||
source="/hls/stream.m3u8"
|
||||
online={online}
|
||||
title={streamTitle || name}
|
||||
/>
|
||||
)}
|
||||
{!online && !appState.appLoading && (
|
||||
<div id="offline-message">
|
||||
<OfflineBanner
|
||||
showsHeader={false}
|
||||
streamName={name}
|
||||
customText={offlineMessage}
|
||||
notificationsEnabled={browserNotificationsEnabled}
|
||||
fediverseAccount={fediverseAccount}
|
||||
lastLive={lastDisconnectTime}
|
||||
onNotifyClick={() => setShowNotifyModal(true)}
|
||||
onFollowClick={() => setShowFollowModal(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isStreamLive && (
|
||||
<Statusbar
|
||||
online={online}
|
||||
lastConnectTime={lastConnectTime}
|
||||
lastDisconnectTime={lastDisconnectTime}
|
||||
viewerCount={viewerCount}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.root}>
|
||||
<div className={classnames(styles.mainSection, { [styles.offline]: !online })}>
|
||||
{appState.appLoading ? (
|
||||
<Skeleton loading active paragraph={{ rows: 7 }} className={styles.topSectionElement} />
|
||||
) : (
|
||||
<div className="skeleton-placeholder" />
|
||||
)}
|
||||
{online && (
|
||||
<OwncastPlayer
|
||||
source="/hls/stream.m3u8"
|
||||
online={online}
|
||||
title={streamTitle || name}
|
||||
className={styles.topSectionElement}
|
||||
/>
|
||||
)}
|
||||
{!online && !appState.appLoading && (
|
||||
<div id="offline-message">
|
||||
<OfflineBanner
|
||||
showsHeader={false}
|
||||
streamName={name}
|
||||
customText={offlineMessage}
|
||||
notificationsEnabled={supportsBrowserNotifications}
|
||||
fediverseAccount={fediverseAccount}
|
||||
lastLive={lastDisconnectTime}
|
||||
onNotifyClick={() => setShowNotifyModal(true)}
|
||||
onFollowClick={() => setShowFollowModal(true)}
|
||||
className={styles.topSectionElement}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.midSection}>
|
||||
<div className={styles.buttonsLogoTitleSection}>
|
||||
{!isMobile && (
|
||||
<ActionButtonRow>
|
||||
{externalActionButtons}
|
||||
{supportFediverseFeatures && (
|
||||
<FollowButton size="small" onClick={() => setShowFollowModal(true)} />
|
||||
)}
|
||||
{supportsBrowserNotifications && (
|
||||
<NotifyReminderPopup
|
||||
open={showNotifyReminder}
|
||||
notificationClicked={() => setShowNotifyModal(true)}
|
||||
notificationClosed={() => disableNotifyReminderPopup()}
|
||||
>
|
||||
<NotifyButton onClick={() => setShowNotifyModal(true)} />
|
||||
</NotifyReminderPopup>
|
||||
)}
|
||||
</ActionButtonRow>
|
||||
)}
|
||||
)}
|
||||
{isStreamLive ? (
|
||||
<Statusbar
|
||||
online={online}
|
||||
lastConnectTime={lastConnectTime}
|
||||
lastDisconnectTime={lastDisconnectTime}
|
||||
viewerCount={viewerCount}
|
||||
className={classnames(styles.topSectionElement, styles.statusBar)}
|
||||
/>
|
||||
) : (
|
||||
<div className="statusbar-placeholder" />
|
||||
)}
|
||||
<div className={styles.midSection}>
|
||||
<div className={styles.buttonsLogoTitleSection}>
|
||||
{!isMobile && (
|
||||
<ActionButtonRow>
|
||||
{externalActionButtons}
|
||||
{supportFediverseFeatures && (
|
||||
<FollowButton size="small" onClick={() => setShowFollowModal(true)} />
|
||||
)}
|
||||
{supportsBrowserNotifications && (
|
||||
<NotifyReminderPopup
|
||||
open={showNotifyReminder}
|
||||
notificationClicked={() => setShowNotifyModal(true)}
|
||||
notificationClosed={() => disableNotifyReminderPopup()}
|
||||
>
|
||||
<NotifyButton onClick={() => setShowNotifyModal(true)} />
|
||||
</NotifyReminderPopup>
|
||||
)}
|
||||
</ActionButtonRow>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title="Browser Notifications"
|
||||
open={showNotifyModal}
|
||||
afterClose={() => disableNotifyReminderPopup()}
|
||||
handleCancel={() => disableNotifyReminderPopup()}
|
||||
>
|
||||
<BrowserNotifyModal />
|
||||
</Modal>
|
||||
</div>
|
||||
<Modal
|
||||
title="Browser Notifications"
|
||||
open={showNotifyModal}
|
||||
afterClose={() => disableNotifyReminderPopup()}
|
||||
handleCancel={() => disableNotifyReminderPopup()}
|
||||
>
|
||||
<BrowserNotifyModal />
|
||||
</Modal>
|
||||
</div>
|
||||
{isMobile ? (
|
||||
<MobileContent
|
||||
name={name}
|
||||
streamTitle={streamTitle}
|
||||
summary={summary}
|
||||
tags={tags}
|
||||
socialHandles={socialHandles}
|
||||
extraPageContent={extraPageContent}
|
||||
messages={messages}
|
||||
currentUser={currentUser}
|
||||
showChat={showChat}
|
||||
actions={externalActions}
|
||||
setExternalActionToDisplay={externalActionSelected}
|
||||
setShowNotifyPopup={setShowNotifyModal}
|
||||
setShowFollowModal={setShowFollowModal}
|
||||
supportFediverseFeatures={supportFediverseFeatures}
|
||||
supportsBrowserNotifications={supportsBrowserNotifications}
|
||||
/>
|
||||
) : (
|
||||
<DesktopContent
|
||||
name={name}
|
||||
streamTitle={streamTitle}
|
||||
summary={summary}
|
||||
tags={tags}
|
||||
socialHandles={socialHandles}
|
||||
extraPageContent={extraPageContent}
|
||||
setShowFollowModal={setShowFollowModal}
|
||||
supportFediverseFeatures={supportFediverseFeatures}
|
||||
/>
|
||||
)}
|
||||
{!isMobile && <Footer version={version} />}
|
||||
</div>
|
||||
{showChat && !isMobile && <Sidebar />}
|
||||
{isMobile ? (
|
||||
<MobileContent
|
||||
name={name}
|
||||
summary={summary}
|
||||
tags={tags}
|
||||
socialHandles={socialHandles}
|
||||
extraPageContent={extraPageContent}
|
||||
messages={messages}
|
||||
currentUser={currentUser}
|
||||
showChat={showChat}
|
||||
actions={externalActions}
|
||||
setExternalActionToDisplay={externalActionSelected}
|
||||
setShowNotifyPopup={setShowNotifyModal}
|
||||
setShowFollowModal={setShowFollowModal}
|
||||
supportFediverseFeatures={supportFediverseFeatures}
|
||||
supportsBrowserNotifications={supportsBrowserNotifications}
|
||||
notifyItemSelected={() => setShowNotifyModal(true)}
|
||||
followItemSelected={() => setShowFollowModal(true)}
|
||||
externalActionSelected={externalActionSelected}
|
||||
/>
|
||||
) : (
|
||||
<DesktopContent
|
||||
name={name}
|
||||
summary={summary}
|
||||
tags={tags}
|
||||
socialHandles={socialHandles}
|
||||
extraPageContent={extraPageContent}
|
||||
setShowFollowModal={setShowFollowModal}
|
||||
supportFediverseFeatures={supportFediverseFeatures}
|
||||
/>
|
||||
)}
|
||||
{!isMobile && <Footer version={version} />}
|
||||
</div>
|
||||
{showChat && !isMobile && <Sidebar />}
|
||||
</div>
|
||||
{externalActionToDisplay && (
|
||||
<ExternalModal
|
||||
|
||||
81
web/components/ui/Content/DesktopContent.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { TabsProps } from 'antd';
|
||||
import { SocialLink } from '../../../interfaces/social-link.model';
|
||||
import styles from './Content.module.scss';
|
||||
import { CustomPageContent } from '../CustomPageContent/CustomPageContent';
|
||||
import { ContentHeader } from '../../common/ContentHeader/ContentHeader';
|
||||
|
||||
export type DesktopContentProps = {
|
||||
name: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
socialHandles: SocialLink[];
|
||||
extraPageContent: string;
|
||||
setShowFollowModal: (show: boolean) => void;
|
||||
supportFediverseFeatures: boolean;
|
||||
};
|
||||
|
||||
// lazy loaded components
|
||||
|
||||
const Tabs: ComponentType<TabsProps> = dynamic(() => import('antd').then(mod => mod.Tabs), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FollowerCollection = dynamic(
|
||||
() =>
|
||||
import('../followers/FollowerCollection/FollowerCollection').then(
|
||||
mod => mod.FollowerCollection,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export const DesktopContent: FC<DesktopContentProps> = ({
|
||||
name,
|
||||
summary,
|
||||
tags,
|
||||
socialHandles,
|
||||
extraPageContent,
|
||||
setShowFollowModal,
|
||||
supportFediverseFeatures,
|
||||
}) => {
|
||||
const aboutTabContent = (
|
||||
<div className={styles.bottomPageContentContainer}>
|
||||
<CustomPageContent content={extraPageContent} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const followersTabContent = (
|
||||
<div className={styles.bottomPageContentContainer}>
|
||||
<FollowerCollection name={name} onFollowButtonClick={() => setShowFollowModal(true)} />
|
||||
</div>
|
||||
);
|
||||
const items = [!!extraPageContent && { label: 'About', key: '2', children: aboutTabContent }];
|
||||
if (supportFediverseFeatures) {
|
||||
items.push({ label: 'Followers', key: '3', children: followersTabContent });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.lowerHalf} id="skip-to-content">
|
||||
<ContentHeader
|
||||
name={name}
|
||||
summary={summary}
|
||||
tags={tags}
|
||||
links={socialHandles}
|
||||
logo="/logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.lowerSection}>
|
||||
{items.length > 1 ? (
|
||||
<Tabs defaultActiveKey="0" items={items} />
|
||||
) : (
|
||||
!!extraPageContent && aboutTabContent
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
140
web/components/ui/Content/MobileContent.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Skeleton, TabsProps } from 'antd';
|
||||
import { SocialLink } from '../../../interfaces/social-link.model';
|
||||
import styles from './Content.module.scss';
|
||||
import { CustomPageContent } from '../CustomPageContent/CustomPageContent';
|
||||
import { ContentHeader } from '../../common/ContentHeader/ContentHeader';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import { CurrentUser } from '../../../interfaces/current-user';
|
||||
import { ActionButtonMenu } from '../../action-buttons/ActionButtonMenu/ActionButtonMenu';
|
||||
import { ExternalAction } from '../../../interfaces/external-action';
|
||||
|
||||
export type MobileContentProps = {
|
||||
name: string;
|
||||
summary: string;
|
||||
tags: string[];
|
||||
socialHandles: SocialLink[];
|
||||
extraPageContent: string;
|
||||
notifyItemSelected: () => void;
|
||||
followItemSelected: () => void;
|
||||
setExternalActionToDisplay: (action: ExternalAction) => void;
|
||||
setShowNotifyPopup: (show: boolean) => void;
|
||||
setShowFollowModal: (show: boolean) => void;
|
||||
supportFediverseFeatures: boolean;
|
||||
messages: ChatMessage[];
|
||||
currentUser: CurrentUser;
|
||||
showChat: boolean;
|
||||
actions: ExternalAction[];
|
||||
externalActionSelected: (action: ExternalAction) => void;
|
||||
supportsBrowserNotifications: boolean;
|
||||
};
|
||||
|
||||
// lazy loaded components
|
||||
|
||||
const Tabs: ComponentType<TabsProps> = dynamic(() => import('antd').then(mod => mod.Tabs), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FollowerCollection = dynamic(
|
||||
() =>
|
||||
import('../followers/FollowerCollection/FollowerCollection').then(
|
||||
mod => mod.FollowerCollection,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
const ChatContainer = dynamic(
|
||||
() => import('../../chat/ChatContainer/ChatContainer').then(mod => mod.ChatContainer),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export const MobileContent: FC<MobileContentProps> = ({
|
||||
name,
|
||||
summary,
|
||||
tags,
|
||||
socialHandles,
|
||||
extraPageContent,
|
||||
messages,
|
||||
currentUser,
|
||||
showChat,
|
||||
actions,
|
||||
setExternalActionToDisplay,
|
||||
setShowNotifyPopup,
|
||||
setShowFollowModal,
|
||||
supportFediverseFeatures,
|
||||
supportsBrowserNotifications,
|
||||
}) => {
|
||||
if (!currentUser) {
|
||||
return <Skeleton loading active paragraph={{ rows: 7 }} />;
|
||||
}
|
||||
const { id, displayName } = currentUser;
|
||||
|
||||
const chatContent = showChat && (
|
||||
<ChatContainer
|
||||
messages={messages}
|
||||
usernameToHighlight={displayName}
|
||||
chatUserId={id}
|
||||
isModerator={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const aboutTabContent = (
|
||||
<>
|
||||
<ContentHeader name={name} summary={summary} tags={tags} links={socialHandles} logo="/logo" />
|
||||
{!!extraPageContent && (
|
||||
<div className={styles.bottomPageContentContainer}>
|
||||
<CustomPageContent content={extraPageContent} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const followersTabContent = (
|
||||
<div className={styles.bottomPageContentContainer}>
|
||||
<FollowerCollection name={name} onFollowButtonClick={() => setShowFollowModal(true)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const items = [];
|
||||
if (showChat) {
|
||||
items.push({ label: 'Chat', key: '0', children: chatContent });
|
||||
}
|
||||
items.push({ label: 'About', key: '2', children: aboutTabContent });
|
||||
if (supportFediverseFeatures) {
|
||||
items.push({ label: 'Followers', key: '3', children: followersTabContent });
|
||||
}
|
||||
|
||||
const replacementTabBar = (props, DefaultTabBar) => (
|
||||
<div className={styles.replacementBar}>
|
||||
<DefaultTabBar {...props} className={styles.defaultTabBar} />
|
||||
<ActionButtonMenu
|
||||
className={styles.actionButtonMenu}
|
||||
showFollowItem={supportFediverseFeatures}
|
||||
showNotifyItem={supportsBrowserNotifications}
|
||||
actions={actions}
|
||||
externalActionSelected={setExternalActionToDisplay}
|
||||
notifyItemSelected={() => setShowNotifyPopup(true)}
|
||||
followItemSelected={() => setShowFollowModal(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.lowerSectionMobile}>
|
||||
{items.length > 1 ? (
|
||||
<Tabs
|
||||
className={styles.tabs}
|
||||
defaultActiveKey="0"
|
||||
items={items}
|
||||
renderTabBar={replacementTabBar}
|
||||
/>
|
||||
) : (
|
||||
aboutTabContent
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ export type CrossfadeImageProps = {
|
||||
height: string;
|
||||
objectFit?: ObjectFit;
|
||||
duration?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const imgStyle: React.CSSProperties = {
|
||||
@@ -22,6 +23,7 @@ export const CrossfadeImage: FC<CrossfadeImageProps> = ({
|
||||
height,
|
||||
objectFit = 'fill',
|
||||
duration = '1s',
|
||||
className,
|
||||
}) => {
|
||||
const spanStyle: React.CSSProperties = useMemo(
|
||||
() => ({
|
||||
@@ -52,7 +54,7 @@ export const CrossfadeImage: FC<CrossfadeImageProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<span style={spanStyle}>
|
||||
<span style={spanStyle} className={className}>
|
||||
{[...srcs, nextSrc].map(
|
||||
(singleSrc, index) =>
|
||||
singleSrc !== '' && (
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
@import 'styles/mixins.scss';
|
||||
|
||||
.pageContentContainer {
|
||||
@include flexCenter;
|
||||
}
|
||||
|
||||
.customPageContent {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6em;
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
padding: calc(2 * var(--content-padding));
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
width: 100%;
|
||||
background-color: var(--theme-color-components-content-background);
|
||||
|
||||
hr {
|
||||
margin: 1.35em 0;
|
||||
border: 0;
|
||||
border-top: solid 1px var(--theme-color-components-content-background);
|
||||
border-top: solid 1px var(--theme-color-palette-6);
|
||||
}
|
||||
|
||||
div.summary {
|
||||
|
||||
@@ -18,12 +18,12 @@ const Template: ComponentStory<typeof CustomPageContent> = args => (
|
||||
|
||||
export const Example1 = Template.bind({});
|
||||
Example1.args = {
|
||||
content: `"\u003cp\u003eOwncast TV is a 24/7 live stream run by the Owncast project as an example of the software in use. Learn more about how you can have your own live stream that you completely control at \u003ca href=\"https://owncast.online\"\u003eowncast.online\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThis example instance shows how you can customize the page by changing things like fonts and colors as well as how you can add custom action buttons such as a donation button.\u003c/p\u003e\n\u003cp\u003eStay tuned in to learn about Owncast, hear from some streamers about their experiences using it, some bits and pieces of Owncast promo material, and highlights from other projects that are pretty cool.\u003c/p\u003e\n\u003cp\u003eBut when you've seen what we have to share with you, do yourself a favor and visit the \u003ca href=\"https://directory.owncast.online\"\u003eOwncast Directory\u003c/a\u003e and find an awesome stream to check out!\u003c/p\u003e\n\u003chr\u003e\n\u003ch2\u003eLinks to content seen in this stream\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://owncast.online/quickstart/\"\u003eOwncast Install Quickstart\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://directory.owncast.online\"\u003eOwncast Directory\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://creativecommons.org\"\u003eCreative Commons\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tilvids.com\"\u003eTILVids\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://studio.blender.org/\"\u003eBlender Studio\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://archive.org/details/computerchronicles\"\u003eComputer Chronicles\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://joinmastodon.org\"\u003eMastodon\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e",`,
|
||||
content: `\u003cp\u003eOwncast TV is a 24/7 live stream run by the Owncast project as an example of the software in use. Learn more about how you can have your own live stream that you completely control at \u003ca href=\"https://owncast.online\"\u003eowncast.online\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThis example instance shows how you can customize the page by changing things like fonts and colors as well as how you can add custom action buttons such as a donation button.\u003c/p\u003e\n\u003cp\u003eStay tuned in to learn about Owncast, hear from some streamers about their experiences using it, some bits and pieces of Owncast promo material, and highlights from other projects that are pretty cool.\u003c/p\u003e\n\u003cp\u003eBut when you've seen what we have to share with you, do yourself a favor and visit the \u003ca href=\"https://directory.owncast.online\"\u003eOwncast Directory\u003c/a\u003e and find an awesome stream to check out!\u003c/p\u003e\n\u003chr\u003e\n\u003ch2\u003eLinks to content seen in this stream\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://owncast.online/quickstart/\"\u003eOwncast Install Quickstart\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://directory.owncast.online\"\u003eOwncast Directory\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://creativecommons.org\"\u003eCreative Commons\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tilvids.com\"\u003eTILVids\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://studio.blender.org/\"\u003eBlender Studio\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://archive.org/details/computerchronicles\"\u003eComputer Chronicles\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://joinmastodon.org\"\u003eMastodon\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e",`,
|
||||
};
|
||||
|
||||
export const Example2 = Template.bind({});
|
||||
Example2.args = {
|
||||
content: `"<h1>WHAT IS HAPPENING HERE</h1>\n<p>Game That Tune Radio is live with fantastic video game music streaming around the clock! We've got music from NES, SNES, Sega Genesis, Nintendo 64, Playstation, PC, and more coming all the time! If it's been featured on our podcast, it's gonna be on this stream! We only play three songs from each game on our podcast, and we decided that everyone needs more tunes!</p>\n<p>We'll be updating this livestream with new games as they're played on the show, including your requests! To get priority in requesting games for the show, check out <a href=\"https://www.patreon.com/GameThatTune\">https://www.patreon.com/GameThatTune</a></p>\n<p>Be sure to check out our live recordings of the Game That Tune podcast! We broadcast every Wednesday night at 9 PM EST on our YouTube channel as well as <a href=\"https://www.twitch.tv/GameThatTune\">https://www.twitch.tv/GameThatTune</a> and <a href=\"https://www.facebook.com/GameThatTune\">https://www.facebook.com/GameThatTune</a>\nTune in and join in on the fun! Find the podcast in iTunes every Wednesday morning or head to <a href=\"https://www.gamethattune.com\">https://www.gamethattune.com</a>!</p>\n<p>Visit <a href=\"https://www.patreon.com/GameThatTune\">https://www.patreon.com/GameThatTune</a> to help us keep up this live stream and upgrade our equipment for the live show! We've got exclusive mixtapes for our patrons, and lots more stuff planned for the future, so consider helping us out!</p>\n<h1>HOW IT WORKS</h1>\n<p>Featuring music from over 1000 games! Check out <a href=\"https://music.gamethattune.com/songs\">https://music.gamethattune.com/songs</a> for the full list and make a request from your favorite game!</p>\n<p>Now that you've seen the list of games, make a request in the chat!</p>\n<p><code>!sr</code> + anything = general search<br>\n<code>!gr</code> + game title = random song from matching game<br>\n<code>!cr</code> + composer name = random song from matching composer<br>\n<code>!tr</code> + anything = random result only searching song titles<br>\n<code>!rr</code> + anything = random result from all searchable fields<br>\n<code>!game gtt</code> = starts a round of our guessing game for bonus points!</p>\n<p>We have gifs!</p>\n<p>Wanna see your favorite gif on screen? type <code>!summon</code> followed by the gif name! Want your favorite gif to take over the video? Type <code>!spawn</code> followed by the gif name!</p>\n<p>Still have questions? Ask the chatbot! type <code>!info</code> to...wait for it...get more info!</p>\n<p>Thanks for listening!</p>"`,
|
||||
content: `<h1>WHAT IS HAPPENING HERE</h1>\n<p>Game That Tune Radio is live with fantastic video game music streaming around the clock! We've got music from NES, SNES, Sega Genesis, Nintendo 64, Playstation, PC, and more coming all the time! If it's been featured on our podcast, it's gonna be on this stream! We only play three songs from each game on our podcast, and we decided that everyone needs more tunes!</p>\n<p>We'll be updating this livestream with new games as they're played on the show, including your requests! To get priority in requesting games for the show, check out <a href=\"https://www.patreon.com/GameThatTune\">https://www.patreon.com/GameThatTune</a></p>\n<p>Be sure to check out our live recordings of the Game That Tune podcast! We broadcast every Wednesday night at 9 PM EST on our YouTube channel as well as <a href=\"https://www.twitch.tv/GameThatTune\">https://www.twitch.tv/GameThatTune</a> and <a href=\"https://www.facebook.com/GameThatTune\">https://www.facebook.com/GameThatTune</a>\nTune in and join in on the fun! Find the podcast in iTunes every Wednesday morning or head to <a href=\"https://www.gamethattune.com\">https://www.gamethattune.com</a>!</p>\n<p>Visit <a href=\"https://www.patreon.com/GameThatTune\">https://www.patreon.com/GameThatTune</a> to help us keep up this live stream and upgrade our equipment for the live show! We've got exclusive mixtapes for our patrons, and lots more stuff planned for the future, so consider helping us out!</p>\n<h1>HOW IT WORKS</h1>\n<p>Featuring music from over 1000 games! Check out <a href=\"https://music.gamethattune.com/songs\">https://music.gamethattune.com/songs</a> for the full list and make a request from your favorite game!</p>\n<p>Now that you've seen the list of games, make a request in the chat!</p>\n<p><code>!sr</code> + anything = general search<br>\n<code>!gr</code> + game title = random song from matching game<br>\n<code>!cr</code> + composer name = random song from matching composer<br>\n<code>!tr</code> + anything = random result only searching song titles<br>\n<code>!rr</code> + anything = random result from all searchable fields<br>\n<code>!game gtt</code> = starts a round of our guessing game for bonus points!</p>\n<p>We have gifs!</p>\n<p>Wanna see your favorite gif on screen? type <code>!summon</code> followed by the gif name! Want your favorite gif to take over the video? Type <code>!spawn</code> followed by the gif name!</p>\n<p>Still have questions? Ask the chatbot! type <code>!info</code> to...wait for it...get more info!</p>\n<p>Thanks for listening!</p>"`,
|
||||
};
|
||||
|
||||
export const Example3 = Template.bind({});
|
||||
|
||||
@@ -7,7 +7,7 @@ export type CustomPageContentProps = {
|
||||
};
|
||||
|
||||
export const CustomPageContent: FC<CustomPageContentProps> = ({ content }) => (
|
||||
<div className={styles.pageContentContainer} id="custom-page-content">
|
||||
<div id="custom-page-content">
|
||||
<div className={styles.customPageContent} dangerouslySetInnerHTML={{ __html: content }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
box-shadow: 0px 1px 3px 1px rgb(0 0 0 / 10%);
|
||||
background-color: var(--theme-color-background-header);
|
||||
|
||||
h1 {
|
||||
margin-top: unset;
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
@include screen(mobile) {
|
||||
--header-height: 3.85rem;
|
||||
}
|
||||
@@ -38,7 +43,9 @@
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 70vw;
|
||||
// 6rem is an overapproximation of the width of
|
||||
// the user menu
|
||||
max-width: min(70vw, calc(100vw - 6rem));
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -57,3 +64,8 @@
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.offlineTag {
|
||||
cursor: default;
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
}
|
||||
|
||||
@@ -22,12 +22,7 @@ export type HeaderComponentProps = {
|
||||
online: boolean;
|
||||
};
|
||||
|
||||
export const Header: FC<HeaderComponentProps> = ({
|
||||
name = 'Your stream title',
|
||||
chatAvailable,
|
||||
chatDisabled,
|
||||
online,
|
||||
}) => (
|
||||
export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisabled, online }) => (
|
||||
<header className={cn([`${styles.header}`], 'global-header')}>
|
||||
{online ? (
|
||||
<Link href="#player" className={styles.skipLink}>
|
||||
@@ -55,7 +50,7 @@ export const Header: FC<HeaderComponentProps> = ({
|
||||
{chatAvailable && !chatDisabled && <UserDropdown />}
|
||||
{!chatAvailable && !chatDisabled && (
|
||||
<Tooltip title="Chat is available when the stream is live." placement="left">
|
||||
<Tag style={{ cursor: 'pointer' }}>Chat offline</Tag>
|
||||
<Tag className={styles.offlineTag}>Chat offline</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</header>
|
||||
|
||||
@@ -11,3 +11,4 @@
|
||||
background-color: var(--theme-color-components-modal-content-background);
|
||||
color: var(--theme-color-components-modal-content-text);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
@include screen(tablet) {
|
||||
font-size: 1.2rem;
|
||||
padding: 1em;
|
||||
margin: 1rem auto;
|
||||
margin: 1rem 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Divider } from 'antd';
|
||||
import { FC } from 'react';
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||
import dynamic from 'next/dynamic';
|
||||
import classNames from 'classnames';
|
||||
import styles from './OfflineBanner.module.scss';
|
||||
|
||||
// Lazy loaded components
|
||||
@@ -20,6 +21,7 @@ export type OfflineBannerProps = {
|
||||
showsHeader?: boolean;
|
||||
onNotifyClick?: () => void;
|
||||
onFollowClick?: () => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const OfflineBanner: FC<OfflineBannerProps> = ({
|
||||
@@ -31,6 +33,7 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
|
||||
showsHeader = true,
|
||||
onNotifyClick,
|
||||
onFollowClick,
|
||||
className,
|
||||
}) => {
|
||||
let text;
|
||||
if (customText) {
|
||||
@@ -74,7 +77,7 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="offline-banner" className={styles.outerContainer}>
|
||||
<div id="offline-banner" className={classNames(styles.outerContainer, className)}>
|
||||
<div className={styles.innerContainer}>
|
||||
{showsHeader && (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,7 @@ import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||
import intervalToDuration from 'date-fns/intervalToDuration';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Statusbar.module.scss';
|
||||
import { pluralize } from '../../../utils/helpers';
|
||||
|
||||
@@ -16,6 +17,7 @@ export type StatusbarProps = {
|
||||
lastConnectTime?: Date;
|
||||
lastDisconnectTime?: Date;
|
||||
viewerCount: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function makeDurationString(lastConnectTime: Date): string {
|
||||
@@ -43,6 +45,7 @@ export const Statusbar: FC<StatusbarProps> = ({
|
||||
lastConnectTime,
|
||||
lastDisconnectTime,
|
||||
viewerCount,
|
||||
className,
|
||||
}) => {
|
||||
const [, setNow] = useState(new Date());
|
||||
|
||||
@@ -75,7 +78,7 @@ export const Statusbar: FC<StatusbarProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.statusbar} role="status">
|
||||
<div className={classNames(styles.statusbar, className)} role="status">
|
||||
<div>{onlineMessage}</div>
|
||||
<div>{rightSideMessage}</div>
|
||||
</div>
|
||||
|
||||