mirror of
https://github.com/owncast/owncast.git
synced 2024-11-25 22:31:09 +03:00
Give chat a min-height that other elements yield to on mobile clients (#2676)
* Add className prop to some components * Give mobile chatbox height priority over other elements * Optimize for mobile landscape mode * Make thumbnail background black * Fix overflow issues on narrow screens * Adjust layout for offline mode on mobile * Fix main content width on Desktop * Fix offline layout for desktop
This commit is contained in:
parent
c9773091a2
commit
25119561fb
11 changed files with 176 additions and 118 deletions
|
@ -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;
|
||||
|
@ -44,6 +67,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.topSectionElement {
|
||||
background-color: var(--theme-color-components-video-background);
|
||||
}
|
||||
|
||||
.statusBar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.leftCol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -53,13 +84,6 @@
|
|||
display: grid;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
grid-template-rows: 1fr auto;
|
||||
}
|
||||
|
||||
.replacementBar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -331,105 +332,110 @@ 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={browserNotificationsEnabled}
|
||||
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}
|
||||
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 />}
|
||||
</div>
|
||||
{externalActionToDisplay && (
|
||||
<ExternalModal
|
||||
|
|
|
@ -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 !== '' && (
|
||||
|
|
|
@ -38,7 +38,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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
aspect-ratio: 16 / 9;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
height: unset;
|
||||
height: 100%;
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { FC, useEffect } from 'react';
|
|||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { VideoJsPlayerOptions } from 'video.js';
|
||||
import classNames from 'classnames';
|
||||
import { VideoJS } from '../VideoJS/VideoJS';
|
||||
import ViewerPing from '../viewer-ping';
|
||||
import { VideoPoster } from '../VideoPoster/VideoPoster';
|
||||
|
@ -26,6 +27,7 @@ export type OwncastPlayerProps = {
|
|||
online: boolean;
|
||||
initiallyMuted?: boolean;
|
||||
title: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
async function getVideoSettings() {
|
||||
|
@ -45,6 +47,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
|
|||
online,
|
||||
initiallyMuted = false,
|
||||
title,
|
||||
className,
|
||||
}) => {
|
||||
const playerRef = React.useRef(null);
|
||||
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
|
||||
|
@ -308,7 +311,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container} id="player">
|
||||
<div className={classNames(styles.container, className)} id="player">
|
||||
{online && (
|
||||
<div className={styles.player}>
|
||||
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} aria-label={title} />
|
||||
|
|
|
@ -11,7 +11,18 @@
|
|||
.vjs-big-play-button {
|
||||
z-index: 10;
|
||||
color: var(--theme-color-action);
|
||||
font-size: 8rem !important;
|
||||
|
||||
// Setting the font size resizes the video.js
|
||||
// BigPlayButton due to its style definitions
|
||||
// (see https://github.com/videojs/video.js/blob/b306ce614e70e6d3305348d1b69e1434031d73ef/src/css/components/_big-play.scss)
|
||||
// 30vmin determined by trial & error to not cause
|
||||
// overflow with weird (small) x or y dimensions.
|
||||
// min and max are also arbitrary; max was the old
|
||||
// constant value. feel free to change if necessary,
|
||||
// but check short and narrow screen sizes for overflow
|
||||
// issues.
|
||||
font-size: clamp(1rem, 30vmin, 8rem) !important;
|
||||
|
||||
border-color: transparent !important;
|
||||
border-radius: var(--theme-rounded-corners) !important;
|
||||
background-color: transparent !important;
|
||||
|
@ -58,10 +69,10 @@
|
|||
font-family: VideoJS, serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.vjs-icon-placeholder::before {
|
||||
content: '\f110';
|
||||
&::before {
|
||||
content: '\f110';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
.poster {
|
||||
background-color: black;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
background-color: black;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ export const VideoPoster: FC<VideoPosterProps> = ({ online, initialSrc, src: bas
|
|||
objectFit="contain"
|
||||
height="auto"
|
||||
width="100%"
|
||||
className={styles.image}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue