0

embed screen style adjustments (#4063)

* restyle and relayout embed screen to account for smaller screen displays.
- address https://github.com/owncast/owncast/issues/3683 to address overflow issues
- address https://github.com/owncast/owncast/issues/4051 to move the name of the stream

* Javascript formatting autofixes

* clean up; restore package lock

* accommodate cases when there's no follow option; put follow form on one line, but wrap if need

* clean up

* separate out follow form into separate standalone component to be used in multiple places

* improve follow error styling; rm defaultProps for Modal to get rid of warning

* improve styling of follow form and components for legibility

* prettyify scss

* prettyify scss again

* one more time

* prettify ant file

* simplify layout, center everything

* just use gap

* tweak and lint

* lint, again

---------

Co-authored-by: Owncast <owncast@owncast.online>
This commit is contained in:
gingervitis 2025-01-05 17:28:35 -08:00 committed by GitHub
parent 4b1a89bb31
commit 87c7571d5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 270 additions and 261 deletions

View File

@ -0,0 +1,103 @@
/* eslint-disable react/no-unescaped-entities */
import { Input, Button, Alert, Spin, Space } from 'antd';
import { FC, useState } from 'react';
import styles from './FollowModal.module.scss';
import { isValidFediverseAccount } from '../../../utils/validators';
const ENDPOINT = '/api/remotefollow';
export type FollowFormProps = {
handleClose?: () => void;
};
export const FollowForm: FC<FollowFormProps> = ({ handleClose }: FollowFormProps) => {
const [remoteAccount, setRemoteAccount] = useState(null);
const [valid, setValid] = useState(false);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const handleAccountChange = a => {
setRemoteAccount(a);
if (isValidFediverseAccount(a)) {
setValid(true);
} else {
setValid(false);
}
};
const joinButtonPressed = () => {
window.open('https://owncast.online/join-fediverse', '_blank');
};
const remoteFollowButtonPressed = async () => {
if (!valid) {
return;
}
setLoading(true);
try {
const sanitizedAccount = remoteAccount.replace(/^@+/, '');
const request = { account: sanitizedAccount };
const rawResponse = await fetch(ENDPOINT, {
method: 'POST',
body: JSON.stringify(request),
});
const result = await rawResponse.json();
if (result.redirectUrl) {
window.open(result.redirectUrl, '_blank');
handleClose();
}
if (!result.success) {
setErrorMessage(result.message);
setLoading(false);
return;
}
if (!result.redirectUrl) {
setErrorMessage('Unable to follow.');
setLoading(false);
return;
}
} catch (e) {
setErrorMessage(e.message);
}
setLoading(false);
};
return (
<Spin spinning={loading}>
{errorMessage && (
<Alert
message="Follow Error"
description={errorMessage}
type="error"
closable
className={styles.errorAlert}
/>
)}
<div className={styles.inputContainer}>
<div className={styles.instructions}>Enter your username @server to follow</div>
<Input
value={remoteAccount}
size="large"
onChange={e => handleAccountChange(e.target.value)}
placeholder="Your fediverse account @account@server"
defaultValue={remoteAccount}
/>
<div className={styles.footer}>
You'll be redirected to your Fediverse server and asked to confirm the action.
</div>
</div>
<Space className={styles.buttons}>
<Button onClick={joinButtonPressed} type="text">
Join the Fediverse
</Button>
<Button disabled={!valid} type="primary" onClick={remoteFollowButtonPressed}>
Follow
</Button>
</Space>
</Spin>
);
};

View File

@ -9,16 +9,21 @@
margin-top: 10px;
}
.instructions {
.inputContainer {
font-family: var(--theme-text-display-font-family);
font-size: 0.7rem;
margin-bottom: 10px;
.instructions {
font-size: 14px;
font-weight: 600;
margin-top: 5px;
margin-bottom: 5px;
margin: 5px 2px;
}
.footer {
font-size: 0.5rem;
font-size: 10px;
margin: 2px;
color: var(--theme-color-components-primary-button-text-disabled);
}
}
.account {
@ -50,3 +55,17 @@
}
}
}
.errorAlert {
margin-bottom: 1.25rem;
font-family: var(--theme-text-display-font-family);
:global(.ant-alert-message) {
font-size: 14px;
}
:global(.ant-alert-description) {
font-size: 12px;
font-family: monospace;
}
}

View File

@ -1,10 +1,8 @@
/* eslint-disable react/no-unescaped-entities */
import { Input, Button, Alert, Spin, Space } from 'antd';
import { FC, useState } from 'react';
import { Space } from 'antd';
import { FC } from 'react';
import styles from './FollowModal.module.scss';
import { isValidFediverseAccount } from '../../../utils/validators';
const ENDPOINT = '/api/remotefollow';
import { FollowForm } from './FollowForm';
export type FollowModalProps = {
handleClose: () => void;
@ -12,62 +10,7 @@ export type FollowModalProps = {
name: string;
};
export const FollowModal: FC<FollowModalProps> = ({ handleClose, account, name }) => {
const [remoteAccount, setRemoteAccount] = useState(null);
const [valid, setValid] = useState(false);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const handleAccountChange = a => {
setRemoteAccount(a);
if (isValidFediverseAccount(a)) {
setValid(true);
} else {
setValid(false);
}
};
const joinButtonPressed = () => {
window.open('https://owncast.online/join-fediverse', '_blank');
};
const remoteFollowButtonPressed = async () => {
if (!valid) {
return;
}
setLoading(true);
try {
const sanitizedAccount = remoteAccount.replace(/^@+/, '');
const request = { account: sanitizedAccount };
const rawResponse = await fetch(ENDPOINT, {
method: 'POST',
body: JSON.stringify(request),
});
const result = await rawResponse.json();
if (result.redirectUrl) {
window.open(result.redirectUrl, '_blank');
handleClose();
}
if (!result.success) {
setErrorMessage(result.message);
setLoading(false);
return;
}
if (!result.redirectUrl) {
setErrorMessage('Unable to follow.');
setLoading(false);
return;
}
} catch (e) {
setErrorMessage(e.message);
}
setLoading(false);
};
return (
export const FollowModal: FC<FollowModalProps> = ({ handleClose, account, name }) => (
<Space direction="vertical" id="follow-modal">
<div className={styles.header}>
By following this stream you'll get notified on the Fediverse when it goes live. Now is a
@ -77,11 +20,6 @@ export const FollowModal: FC<FollowModalProps> = ({ handleClose, account, name }
</a>
if it's new to you.
</div>
<Spin spinning={loading}>
{errorMessage && (
<Alert message="Follow Error" description={errorMessage} type="error" showIcon />
)}
<div className={styles.account}>
<img src="/logo" alt="logo" className={styles.logo} />
<div className={styles.username}>
@ -90,28 +28,6 @@ export const FollowModal: FC<FollowModalProps> = ({ handleClose, account, name }
</div>
</div>
<div>
<div className={styles.instructions}>Enter your username @server to follow</div>
<Input
value={remoteAccount}
size="large"
onChange={e => handleAccountChange(e.target.value)}
placeholder="Your fediverse account @account@server"
defaultValue={remoteAccount}
/>
<div className={styles.footer}>
You'll be redirected to your Fediverse server and asked to confirm the action.
</div>
</div>
<Space className={styles.buttons}>
<Button disabled={!valid} type="primary" onClick={remoteFollowButtonPressed}>
Follow
</Button>
<Button onClick={joinButtonPressed} type="primary">
Join the Fediverse
</Button>
</Space>
</Spin>
<FollowForm handleClose={handleClose} />
</Space>
);
};

View File

@ -95,11 +95,3 @@ export const Modal: FC<ModalProps> = ({
</AntModal>
);
};
Modal.defaultProps = {
url: undefined,
children: undefined,
handleOk: undefined,
handleCancel: undefined,
afterClose: undefined,
};

View File

@ -1,6 +1,12 @@
@import '../../../styles/mixins';
$short-container-max-height: 480px;
$short-container-min-height: 320px;
$follow-modal-width: 300px;
.offlineContainer {
--text-color: rgb(255 255 255 / 100%);
position: absolute;
width: 100%;
height: 100%;
@ -13,56 +19,108 @@
align-items: center;
justify-content: center;
gap: 16px;
padding: 24px;
font-size: 16px;
@include screen(mobile) {
font-size: 12px;
}
@media (height <= $short-container-max-height) {
font-size: 12px;
}
/* Content */
.content {
color: var(--text-color);
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
gap: 8px;
gap: 2rem;
padding: 24px;
text-align: center;
width: 100vw;
height: 100vh;
max-width: 1024px;
max-height: 576px;
justify-content: center;
.headerContainer {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
margin: 0;
font-size: 1em;
}
.messageContainer {
width: 100%;
--description-height: auto;
@media (height <= $short-container-min-height) {
--description-height: 30vh;
margin: 0;
}
min-height: var(--description-height);
--gradient-mask: linear-gradient(to top, rgb(0 0 0 / 0%), rgb(0 0 0 / 100%) 17%);
overflow: hidden;
overflow-y: auto;
max-height: var(--description-height);
mask-image: var(--gradient-mask);
mask-repeat: repeat-x;
mask-size: auto var(--description-height);
mask-position: top;
padding-bottom: 0.5rem;
}
/* Message */
.message {
color: rgb(255 255 255 / 100%);
color: var(--text-color);
font-family: var(--theme-text-body-font-family);
font-style: normal;
font-size: 16px;
font-weight: 400;
font-size: 1em;
line-height: 1.375;
letter-spacing: 0;
text-decoration: none;
text-transform: none;
display: block;
margin: auto;
@include screen(desktop) {
width: 80%;
}
}
/* Heading */
.heading {
color: rgb(255 255 255 / 100%);
.offlineTitle {
color: var(--text-color);
font-family: var(--theme-text-display-font-family);
font-style: normal;
font-size: 24px;
font-weight: 500;
font-size: 1.375em;
font-weight: 600;
line-height: 1.125;
letter-spacing: -0.125px;
text-decoration: none;
text-transform: none;
margin: 0;
}
/* Page Logo */
.pageLogo {
position: relative;
width: 10vw;
height: 10vw;
min-height: 64px;
min-width: 64px;
min-height: 3em;
min-width: 3em;
max-height: 100px;
max-width: 100px;
border-radius: 96px;
background-color: rgb(255 255 255 / 100%);
border: 5px solid rgb(18 22 29 / 100%);
border: 2px solid rgb(18 22 29 / 100%);
display: flex;
flex-flow: row nowrap;
align-items: flex-start;
@ -73,26 +131,26 @@
background-position: center;
}
/* Page Name */
.pageName {
color: rgb(255 255 255 / 100%);
/* Stream Name */
.streamName {
color: var(--text-color);
font-family: var(--theme-text-display-font-family);
font-style: normal;
font-size: 20px;
font-size: 1.25em;
font-weight: 500;
line-height: 1.1875;
letter-spacing: -0.0625px;
text-decoration: none;
text-transform: none;
}
text-align: left;
}
.submitButton {
margin-top: 10px;
}
.followButton {
display: none;
.footer {
color: white;
padding: 5px;
@media (width > $follow-modal-width) {
display: block;
}
}
}
}

View File

@ -3,12 +3,12 @@
import { FC, useEffect, useState } from 'react';
import classNames from 'classnames';
import Head from 'next/head';
import { Button, Input, Space, Spin, Alert, Typography } from 'antd';
import { Button, Typography } from 'antd';
import styles from './OfflineEmbed.module.scss';
import { isValidFediverseAccount } from '../../../utils/validators';
import { Modal } from '../Modal/Modal';
import { FollowForm } from '../../modals/FollowModal/FollowForm';
const { Title } = Typography;
const ENDPOINT = '/api/remotefollow';
export type OfflineEmbedProps = {
streamName: string;
@ -20,8 +20,6 @@ export type OfflineEmbedProps = {
enum EmbedMode {
CannotFollow = 1,
CanFollow,
FollowPrompt,
InProgress,
}
export const OfflineEmbed: FC<OfflineEmbedProps> = ({
@ -31,10 +29,7 @@ export const OfflineEmbed: FC<OfflineEmbedProps> = ({
supportsFollows,
}) => {
const [currentMode, setCurrentMode] = useState(EmbedMode.CanFollow);
const [remoteAccount, setRemoteAccount] = useState(null);
const [valid, setValid] = useState(false);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [showFollowModal, setShowFollowModal] = useState(false);
useEffect(() => {
if (!supportsFollows) {
@ -45,53 +40,7 @@ export const OfflineEmbed: FC<OfflineEmbedProps> = ({
}, [supportsFollows]);
const followButtonPressed = async () => {
setCurrentMode(EmbedMode.FollowPrompt);
};
const remoteFollowButtonPressed = async () => {
setLoading(true);
setCurrentMode(EmbedMode.CannotFollow);
try {
const sanitizedAccount = remoteAccount.replace(/^@+/, '');
const request = { account: sanitizedAccount };
const rawResponse = await fetch(ENDPOINT, {
method: 'POST',
body: JSON.stringify(request),
});
const result = await rawResponse.json();
if (result.redirectUrl) {
window.open(result.redirectUrl, '_blank');
}
if (!result.success) {
setErrorMessage(result.message);
setLoading(false);
return;
}
if (!result.redirectUrl) {
setErrorMessage('Unable to follow.');
setLoading(false);
return;
}
} catch (e) {
setErrorMessage(e.message);
}
setLoading(false);
};
const handleErrorClose = () => {
setErrorMessage('');
setCurrentMode(EmbedMode.FollowPrompt);
};
const handleAccountChange = a => {
setRemoteAccount(a);
if (isValidFediverseAccount(a)) {
setValid(true);
} else {
setValid(false);
}
setShowFollowModal(true);
};
return (
@ -100,64 +49,34 @@ export const OfflineEmbed: FC<OfflineEmbedProps> = ({
<title>{streamName}</title>
</Head>
<div className={classNames(styles.offlineContainer)}>
<Spin spinning={loading}>
<div className={styles.content}>
<div className={styles.heading}>This stream is not currently live.</div>
<div className={styles.message} dangerouslySetInnerHTML={{ __html: subtitle }} />
<Title level={1} className={styles.headerContainer}>
<div className={styles.pageLogo} style={{ backgroundImage: `url(${image})` }} />
<div className={styles.pageName}>{streamName}</div>
<div className={styles.streamName}>{streamName}</div>
</Title>
{errorMessage && (
<Alert
message="Follow Error"
description={errorMessage}
type="error"
showIcon
closable
onClose={handleErrorClose}
/>
)}
<div className={styles.messageContainer}>
<Title level={2} className={styles.offlineTitle}>
This stream is not currently live.
</Title>
<div className={styles.message} dangerouslySetInnerHTML={{ __html: subtitle }} />
</div>
{currentMode === EmbedMode.CanFollow && (
<Button className={styles.submitButton} type="primary" onClick={followButtonPressed}>
<>
<Button className={styles.followButton} type="primary" onClick={followButtonPressed}>
Follow Server
</Button>
)}
{currentMode === EmbedMode.InProgress && (
<Title level={4} className={styles.heading}>
Follow the instructions on your Fediverse server to complete the follow.
</Title>
)}
{currentMode === EmbedMode.FollowPrompt && (
<div>
<Input
value={remoteAccount}
size="large"
onChange={e => handleAccountChange(e.target.value)}
placeholder="Your fediverse account @account@server"
defaultValue={remoteAccount}
/>
<div className={styles.footer}>
You&apos;ll be redirected to your Fediverse server and asked to confirm the
action.
</div>
<Space className={styles.buttons}>
<Button
className={styles.submitButton}
disabled={!valid}
type="primary"
onClick={remoteFollowButtonPressed}
<Modal
title={`Follow ${streamName}`}
open={showFollowModal}
handleCancel={() => setShowFollowModal(false)}
>
Submit and Follow
</Button>
</Space>
</div>
<FollowForm />
</Modal>
</>
)}
</div>
</Spin>
</div>
</div>
);

View File

@ -11,6 +11,10 @@ HEADER
BUTTONS
// ------------------------- */
.ant-btn {
font-size: 0.85rem;
}
.ant-btn[disabled] {
background-color: var(--theme-color-components-secondary-button-background-disabled);
color: var(--theme-color-components-secondary-button-text-disabled);
@ -40,8 +44,6 @@ BUTTONS
}
.ant-btn-primary {
height: 2rem;
font-size: 0.85rem;
border-width: 2px;
border-radius: var(--theme-rounded-corners);
border-color: var(--theme-color-components-primary-button-border);