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:
Michael David Kuckuk 2023-02-09 03:50:58 +01:00 committed by GitHub
parent c9773091a2
commit 25119561fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 118 deletions

View file

@ -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;

View file

@ -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

View file

@ -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 !== '' && (

View file

@ -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;
}

View file

@ -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 && (
<>

View file

@ -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>

View file

@ -8,7 +8,7 @@
aspect-ratio: 16 / 9;
@media (max-width: 1200px) {
height: unset;
height: 100%;
max-height: 75vh;
}

View file

@ -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} />

View file

@ -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';
}
}
}

View file

@ -1,7 +1,10 @@
.poster {
background-color: black;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
}
.image {
background-color: black;
}

View file

@ -36,6 +36,7 @@ export const VideoPoster: FC<VideoPosterProps> = ({ online, initialSrc, src: bas
objectFit="contain"
height="auto"
width="100%"
className={styles.image}
/>
)}
</div>