Added chat tab on mobile layouts and other changes to mobile UI

This commit is contained in:
t1enne 2022-07-08 09:10:18 +02:00
parent d47084f257
commit ffc73f2760
20 changed files with 183 additions and 146 deletions

View file

@ -1,10 +1,10 @@
.row { .row {
margin: 5px; padding: .3rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
button { button {
margin-left: 5px; margin-left: .3rem;
margin-right: 5px; margin-right: .3rem;
} }
} }

View file

@ -8,7 +8,7 @@ import s from './ActionButton.module.scss';
import { clientConfigStateAtom } from '../stores/ClientConfigStore'; import { clientConfigStateAtom } from '../stores/ClientConfigStore';
import { ClientConfig } from '../../interfaces/client-config.model'; import { ClientConfig } from '../../interfaces/client-config.model';
export default function FollowButton() { export default function FollowButton(props: any) {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom); const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const { name } = clientConfig; const { name } = clientConfig;
@ -20,8 +20,9 @@ export default function FollowButton() {
return ( return (
<> <>
<Button <Button
{...props}
type="primary" type="primary"
className={`${s.button}`} className={s.button}
icon={<HeartFilled />} icon={<HeartFilled />}
onClick={buttonClicked} onClick={buttonClicked}
> >

View file

@ -67,7 +67,7 @@ export default function ChatContainer(props: Props) {
() => ( () => (
<> <>
<Virtuoso <Virtuoso
style={{ height: isMobile ? 500 : '77vh' }} style={{ height: isMobile ? 500 : '77vh', width: 'auto' }}
ref={chatContainerRef} ref={chatContainerRef}
initialTopMostItemIndex={messages.length - 1} // Force alignment to bottom initialTopMostItemIndex={messages.length - 1} // Force alignment to bottom
data={messages} data={messages}
@ -99,9 +99,11 @@ export default function ChatContainer(props: Props) {
return ( return (
<div> <div>
<div className={s.chatHeader}> {
<span>stream chat</span> // <div className={s.chatHeader}>
</div> // <span>stream chat</span>
// </div>
}
<Spin spinning={loading} indicator={spinIcon}> <Spin spinning={loading} indicator={spinIcon}>
{MessagesTable} {MessagesTable}
</Spin> </Spin>

View file

@ -20,6 +20,17 @@
.inputWrapper { .inputWrapper {
display: flex; display: flex;
position: relative; position: relative;
border-radius: var(--theme-rounded-corners);
outline: 1px solid var(--color-owncast-gray-500);
&:hover {
box-shadow: 0 0 1px 1px var(--color-owncast-gray-300);
}
& > div {
transition: box-shadow .2s ease-in-out;
&:focus {
// box-shadow: 0 0 1px 1px var(--color-owncast-gray-300);
}
}
} }
.emojiButton { .emojiButton {

View file

@ -4,8 +4,8 @@
justify-content: center; justify-content: center;
width: max-content; width: max-content;
svg { svg {
width: 50px; width: clamp(2.5rem, 8vw, 50px);
height: 50px; height: clamp(2.5rem, 8vw, 50px);
} }
} }
@ -14,15 +14,15 @@
border-radius: 50%; border-radius: 50%;
background-color: var(--color-owncast-gray-100); background-color: var(--color-owncast-gray-100);
svg { svg {
width: 40px; width: clamp(2rem, 7vw, 40px);
height: 40px; height: clamp(2rem, 7vw, 40px);
} }
} }
.simple { .simple {
background-color: transparent; background-color: transparent;
svg { svg {
width: 50px; width: clamp(2.5rem, 8vw, 50px);
height: 50px; height: clamp(2.5rem, 8vw, 50px);
} }
} }

View file

@ -0,0 +1,36 @@
.streamInfo {
position: relative;
display: grid;
}
.buttonsLogoTitleSection {
margin-left: 1.5vw;
margin-right: 1.5vw;
}
.pageContentSection {
// background-color: var(--theme-background-secondary);
border-radius: var(--theme-rounded-corners);
width: 100%;
}
.logoTitleSection {
display: flex;
align-items: center;
}
.titleSection {
display: flex;
flex-direction: column;
.title {
font-size: 1.5rem;
font-weight: bold;
color: var(--theme-text-primary);
}
.subtitle {
font-size: 1.6vw;
font-weight: bold;
}
}

View file

@ -0,0 +1,28 @@
import { useRecoilValue } from 'recoil';
import { ClientConfig } from '../../../interfaces/client-config.model';
import { clientConfigStateAtom } from '../../stores/ClientConfigStore';
import { ServerLogo } from '../../ui';
import CategoryIcon from '../../ui/CategoryIcon/CategoryIcon';
import SocialLinks from '../../ui/SocialLinks/SocialLinks';
import s from './StreamInfo.module.scss';
export default function StreamInfo() {
const { socialHandles, name, title, tags } = useRecoilValue<ClientConfig>(clientConfigStateAtom);
return (
<div className={s.streamInfo}>
<div className={s.logoTitleSection}>
<ServerLogo src="/logo" />
<div className={s.titleSection}>
<div className={s.title}>{name}</div>
<div className={s.subtitle}>
{title}
<CategoryIcon tags={tags} />
</div>
<div>{tags.length > 0 && tags.map(tag => <span key={tag}>#{tag}&nbsp;</span>)}</div>
<SocialLinks links={socialHandles} />
</div>
</div>
</div>
);
}

View file

@ -2,9 +2,19 @@
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
&.mobile { &.mobile {
display: block; display: flex;
// height: calc(100vh - 64px); flex-direction: column;
// overflow-y: hidden; height: calc(100vh - 64px);
overflow-y: hidden;
.topHalf {
border: 1px dashed white;
height: calc(40vh - 64px);
overflow: hidden;
}
.lowerHalf {
border: 1px dashed red;
height: 60vh;
}
} }
} }
@ -22,46 +32,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.streamInfo {
position: relative;
display: grid;
}
.buttonsLogoTitleSection {
margin-left: 1.5vw;
margin-right: 1.5vw;
}
.pageContentSection {
background-color: var(--theme-background-secondary);
border-radius: var(--theme-rounded-corners);
margin: 1vw;
padding: 1vw;
width: 100%;
}
.logoTitleSection {
display: flex;
flex-direction: row;
}
.titleSection {
display: flex;
flex-direction: column;
margin-top: 20px;
margin-left: 10px;
.title {
font-size: 1.5rem;
font-weight: bold;
color: var(--theme-text-primary);
}
.subtitle {
font-size: 1.6vw;
font-weight: bold;
}
}
.loadingSpinner { .loadingSpinner {
position: fixed; position: fixed;

View file

@ -1,5 +1,5 @@
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { Layout, Tabs, Spin } from 'antd'; import { Layout, Tabs, Spin } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import cn from 'classnames'; import cn from 'classnames';
@ -31,10 +31,7 @@ import ActionButton from '../../action-buttons/ActionButton';
import Statusbar from '../Statusbar/Statusbar'; import Statusbar from '../Statusbar/Statusbar';
import { ServerStatus } from '../../../interfaces/server-status.model'; import { ServerStatus } from '../../../interfaces/server-status.model';
import { Follower } from '../../../interfaces/follower'; import { Follower } from '../../../interfaces/follower';
import SocialLinks from '../SocialLinks/SocialLinks';
import NotifyReminderPopup from '../NotifyReminderPopup/NotifyReminderPopup'; import NotifyReminderPopup from '../NotifyReminderPopup/NotifyReminderPopup';
import ServerLogo from '../Logo/Logo';
import CategoryIcon from '../CategoryIcon/CategoryIcon';
import OfflineBanner from '../OfflineBanner/OfflineBanner'; import OfflineBanner from '../OfflineBanner/OfflineBanner';
import { AppStateOptions } from '../../stores/application-state'; import { AppStateOptions } from '../../stores/application-state';
import FollowButton from '../../action-buttons/FollowButton'; import FollowButton from '../../action-buttons/FollowButton';
@ -50,14 +47,13 @@ export default function ContentComponent() {
const status = useRecoilValue<ServerStatus>(serverStatusState); const status = useRecoilValue<ServerStatus>(serverStatusState);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom); const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector); const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector);
const setIsMobile = useSetRecoilState<boolean>(isMobileAtom); const [isMobile, setIsMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
const isMobile = useRecoilValue(isMobileAtom);
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom); const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
const online = useRecoilValue<boolean>(isOnlineSelector); const online = useRecoilValue<boolean>(isOnlineSelector);
const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom); const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom);
const chatUserId = useRecoilValue<string>(chatUserIdAtom); const chatUserId = useRecoilValue<string>(chatUserIdAtom);
const { extraPageContent, version, socialHandles, name, title, tags, summary } = clientConfig; const { extraPageContent, version, name, summary } = clientConfig;
const { viewerCount, lastConnectTime, lastDisconnectTime } = status; const { viewerCount, lastConnectTime, lastDisconnectTime } = status;
const [showNotifyReminder, setShowNotifyReminder] = useState(false); const [showNotifyReminder, setShowNotifyReminder] = useState(false);
const [showNotifyPopup, setShowNotifyPopup] = useState(false); const [showNotifyPopup, setShowNotifyPopup] = useState(false);
@ -123,84 +119,73 @@ export default function ContentComponent() {
return ( return (
<Content className={rootClassName}> <Content className={rootClassName}>
<Spin className={s.loadingSpinner} size="large" spinning={appState.appLoading} /> <div className={s.leftContent}>
<Spin className={s.loadingSpinner} size="large" spinning={appState.appLoading} />
<div className={s.topHalf}>
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
{!online && (
<OfflineBanner
name={name}
text="Stream is offline text goes here. Will create a new form to set it in the Admin."
/>
)}
<div className={s.leftCol}> <Statusbar
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />} online={online}
{!online && ( lastConnectTime={lastConnectTime}
<OfflineBanner lastDisconnectTime={lastDisconnectTime}
name={name} viewerCount={viewerCount}
text="Stream is offline text goes here. Will create a new form to set it in the Admin."
/> />
)} <div className={s.buttonsLogoTitleSection}>
<ActionButtonRow>
{externalActionButtons}
<FollowButton size="small" />
<NotifyReminderPopup
visible={showNotifyReminder}
notificationClicked={() => setShowNotifyPopup(true)}
notificationClosed={() => disableNotifyReminderPopup()}
>
<NotifyButton onClick={() => setShowNotifyPopup(true)} />
</NotifyReminderPopup>
</ActionButtonRow>
<Statusbar <Modal
online={online} title="Notify"
lastConnectTime={lastConnectTime} visible={showNotifyPopup}
lastDisconnectTime={lastDisconnectTime} afterClose={() => disableNotifyReminderPopup()}
viewerCount={viewerCount} handleCancel={() => disableNotifyReminderPopup()}
/>
<div className={s.buttonsLogoTitleSection}>
<ActionButtonRow>
{externalActionButtons}
<FollowButton />
<NotifyReminderPopup
visible={showNotifyReminder}
notificationClicked={() => setShowNotifyPopup(true)}
notificationClosed={() => disableNotifyReminderPopup()}
> >
<NotifyButton onClick={() => setShowNotifyPopup(true)} /> <BrowserNotifyModal />
</NotifyReminderPopup> </Modal>
</ActionButtonRow>
<Modal
title="Notify"
visible={showNotifyPopup}
afterClose={() => disableNotifyReminderPopup()}
handleCancel={() => disableNotifyReminderPopup()}
>
<BrowserNotifyModal />
</Modal>
<div className={s.streamInfo}>
<div className={s.logoTitleSection}>
<ServerLogo src="/logo" />
<div className={s.titleSection}>
<div className={s.title}>{name}</div>
<div className={s.subtitle}>
{title}
<CategoryIcon tags={tags} />
</div>
<div>{tags.length > 0 && tags.map(tag => <span key={tag}>#{tag}&nbsp;</span>)}</div>
<SocialLinks links={socialHandles} />
</div>
</div>
</div> </div>
</div>
<Tabs defaultActiveKey="1"> <div className={s.lowerHalf}>
<TabPane tab="About" key="1" className={s.pageContentSection}> <Tabs defaultActiveKey="0">
{isChatVisible && isMobile && (
<TabPane tab="Chat" key="0" className={s.pageContentSection}>
<div style={{ position: 'relative' }}>
<div className={s.mobileChat}>
<ChatContainer
messages={messages}
loading={appState.chatLoading}
usernameToHighlight={chatDisplayName}
chatUserId={chatUserId}
isModerator={false}
isMobile={isMobile}
/>
</div>
<ChatTextField />
</div>
</TabPane>
)}
<TabPane tab="About" key="2" className={s.pageContentSection}>
<div dangerouslySetInnerHTML={{ __html: summary }} /> <div dangerouslySetInnerHTML={{ __html: summary }} />
<CustomPageContent content={extraPageContent} /> <CustomPageContent content={extraPageContent} />
</TabPane> </TabPane>
<TabPane tab="Followers" key="2" className={s.pageContentSection}> <TabPane tab="Followers" key="3" className={s.pageContentSection}>
<FollowerCollection total={total} followers={followers} /> <FollowerCollection total={total} followers={followers} />
</TabPane> </TabPane>
</Tabs> </Tabs>
{isChatVisible && isMobile && (
<div style={{ position: 'relative' }}>
<div className={s.mobileChat}>
<ChatContainer
messages={messages}
loading={appState.chatLoading}
usernameToHighlight={chatDisplayName}
chatUserId={chatUserId}
isModerator={false}
isMobile={isMobile}
/>
</div>
<ChatTextField />
</div>
)}
<Footer version={version} /> <Footer version={version} />
</div> </div>
</div> </div>

View file

@ -4,7 +4,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
z-index: 1; z-index: 20;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
background-color: var(--default-bg-color); background-color: var(--default-bg-color);
.logo { .logo {

View file

@ -4,8 +4,8 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
width: clamp(5vw, 90px, 120px); width: clamp(4rem, 10vw, 120px);
height: clamp(5vw, 90px, 120px); height: clamp(4rem, 10vw, 120px);
border-radius: 50%; border-radius: 50%;
border-width: 3px; border-width: 3px;
border-style: solid; border-style: solid;

View file

@ -0,0 +1 @@
export { default } from './Logo';

View file

@ -1,17 +1,17 @@
.outerContainer { .outerContainer {
width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.innerContainer { .innerContainer {
width: clamp(200px, 100%, 300px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 50%; ;
background-color: var(--theme-background-secondary); background-color: var(--theme-background-secondary);
margin: 2vw; margin: auto;
border-radius: var(--theme-rounded-corners); border-radius: var(--theme-rounded-corners);
padding: 25px; padding: 1rem;
} }
.header { .header {

View file

@ -1,10 +1,12 @@
.statusbar { .statusbar {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: .8rem;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
justify-content: space-between; justify-content: space-between;
height: 30px; height: 2rem;
width: 100%; width: 100%;
color: var(--color-owncast-gray-300);
background-color: var(--theme-background-secondary); background-color: var(--theme-background-secondary);
} }

View file

@ -3,3 +3,4 @@ export { default as Sidebar } from './Sidebar/index';
export { default as Footer } from './Footer/index'; export { default as Footer } from './Footer/index';
export { default as Content } from './Content/index'; export { default as Content } from './Content/index';
export { default as ModIcon } from './ModIcon'; export { default as ModIcon } from './ModIcon';
export { default as ServerLogo } from './Logo';

View file

@ -23,7 +23,6 @@ export default function VideoPoster(props: Props) {
if (duration === '0s') { if (duration === '0s') {
setDuration('3s'); setDuration('3s');
} }
setSrc(`${base}?${Date.now()}`); setSrc(`${base}?${Date.now()}`);
}, REFRESH_INTERVAL); }, REFRESH_INTERVAL);
}, []); }, []);
@ -36,7 +35,7 @@ export default function VideoPoster(props: Props) {
<CrossfadeImage <CrossfadeImage
src={src} src={src}
duration={duration} duration={duration}
objectFit="contain" objectFit="cover"
width="100%" width="100%"
height="100%" height="100%"
/> />

View file

@ -4,7 +4,7 @@
} }
.vjs-owncast .vjs-big-play-button { .vjs-owncast .vjs-big-play-button {
z-index: 999999; z-index: 10;
border-color: var(--primary-color) !important; border-color: var(--primary-color) !important;
border-radius: var(--theme-rounded-corners) !important; border-radius: var(--theme-rounded-corners) !important;
} }

View file

@ -3,6 +3,8 @@
// ------------------------- */ // ------------------------- */
.ant-btn { .ant-btn {
height: 2rem;
padding: .3rem 1rem;
background-color: var(--owncast-purple-25); background-color: var(--owncast-purple-25);
font-size: .85rem; font-size: .85rem;
font-weight: bold; font-weight: bold;

View file

@ -9,8 +9,7 @@ body {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: var(--theme-font-family), var(--theme-header-font-family), sans-serif; font-family: var(--theme-font-family), var(--theme-header-font-family), sans-serif;
font-size: clamp(15px, 1.5vw, 16px); font-size: clamp(14px, 1.5vw, 17px);
background-color: var(--default-bg-color); background-color: var(--default-bg-color);
color: var(--default-text-color); color: var(--default-text-color);
} }