mirror of
https://github.com/owncast/owncast.git
synced 2024-10-10 19:16:02 +00:00
Reworked mobile UI for some components
This commit is contained in:
parent
37ad329072
commit
efbe6907ac
@ -65,9 +65,9 @@ export default function ChatContainer(props: Props) {
|
||||
|
||||
const MessagesTable = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<div style={{ height: '100%' }}>
|
||||
<Virtuoso
|
||||
style={{ height: isMobile ? 500 : '77vh', width: 'auto' }}
|
||||
style={{ height: '100%', width: 'auto' }}
|
||||
ref={chatContainerRef}
|
||||
initialTopMostItemIndex={messages.length - 1} // Force alignment to bottom
|
||||
data={messages}
|
||||
@ -92,7 +92,7 @@ export default function ChatContainer(props: Props) {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
),
|
||||
[messages, usernameToHighlight, chatUserId, isModerator, atBottom, isMobile],
|
||||
);
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Props {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function ChatTextField(props: Props) {
|
||||
const [value, setValue] = useState('');
|
||||
const [showEmojis, setShowEmojis] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
placeholder="Type a message here then hit ENTER"
|
||||
/>
|
||||
<button type="button" onClick={() => setShowEmojis(!showEmojis)}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -7,48 +7,62 @@
|
||||
overflow-x: hidden;
|
||||
div[role=textbox] {
|
||||
font-size: 0.9rem;
|
||||
border-radius: .2rem;
|
||||
padding: .6rem;
|
||||
padding-right: calc(0.6rem + 44px);
|
||||
border-radius: .35rem;
|
||||
background-color: var(--color-owncast-gray-700);
|
||||
box-shadow: 0;
|
||||
transition: box-shadow 50ms ease-in-out;
|
||||
&:focus {
|
||||
box-shadow: inset 0px 0px 0x 1px var(--color-owncast-purple-700);
|
||||
outline: 1px solid var(--color-owncast-gray-500) !important;
|
||||
}
|
||||
& > p {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin-right: .3rem;
|
||||
border-radius: .2rem;
|
||||
& > div {
|
||||
transition: box-shadow .2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.emojiButton {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 1rem;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
svg {
|
||||
fill: var(--color-owncast-gray-300);
|
||||
}
|
||||
}
|
||||
|
||||
.submitButtonWrapper {
|
||||
display: flex;
|
||||
padding: 6px 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
display: flex;
|
||||
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);
|
||||
|
||||
|
||||
.mobile {
|
||||
&.root {
|
||||
display: flex;
|
||||
.inputWrapper {
|
||||
flex: 1;
|
||||
}
|
||||
.submitButtonWrapper {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emojiButton {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 1rem;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
svg {
|
||||
fill: var(--color-owncast-gray-300);
|
||||
}
|
||||
}
|
||||
|
||||
.submitButtonWrapper {
|
||||
display: flex;
|
||||
padding: 6px 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@ -4,9 +4,10 @@ import React, { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Transforms, createEditor, BaseEditor, Text } from 'slate';
|
||||
import { Slate, Editable, withReact, ReactEditor } from 'slate-react';
|
||||
import cn from 'classnames';
|
||||
import EmojiPicker from './EmojiPicker';
|
||||
import WebsocketService from '../../../services/websocket-service';
|
||||
import { websocketServiceAtom } from '../../stores/ClientConfigStore';
|
||||
import { isMobileAtom, websocketServiceAtom } from '../../stores/ClientConfigStore';
|
||||
import { MessageType } from '../../../interfaces/socket-events';
|
||||
import s from './ChatTextField.module.scss';
|
||||
|
||||
@ -101,6 +102,7 @@ export default function ChatTextField(props: Props) {
|
||||
// const { value: originalValue } = props;
|
||||
const [showEmojis, setShowEmojis] = useState(false);
|
||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||
const isMobile = useRecoilValue<boolean>(isMobileAtom);
|
||||
const [editor] = useState(() => withImages(withReact(createEditor())));
|
||||
|
||||
const sendMessage = () => {
|
||||
@ -120,7 +122,7 @@ export default function ChatTextField(props: Props) {
|
||||
|
||||
const handleChange = () => {};
|
||||
|
||||
const handleEmojiSelect = e => {
|
||||
const handleEmojiSelect = (e: any) => {
|
||||
ReactEditor.focus(editor);
|
||||
|
||||
if (e.url) {
|
||||
@ -134,7 +136,7 @@ export default function ChatTextField(props: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = e => {
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
@ -142,7 +144,11 @@ export default function ChatTextField(props: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={s.root}>
|
||||
<div
|
||||
className={cn(s.root, {
|
||||
[s.mobile]: isMobile,
|
||||
})}
|
||||
>
|
||||
<div className={s.inputWrapper}>
|
||||
<Slate
|
||||
editor={editor}
|
||||
@ -167,9 +173,13 @@ export default function ChatTextField(props: Props) {
|
||||
</button>
|
||||
</div>
|
||||
<div className={s.submitButtonWrapper}>
|
||||
<Button size="middle" type="primary" icon={<SendOutlined />} onClick={sendMessage}>
|
||||
Send
|
||||
</Button>
|
||||
{isMobile ? (
|
||||
<Button size="large" type="ghost" icon={<SendOutlined />} onClick={sendMessage} />
|
||||
) : (
|
||||
<Button type="primary" icon={<SendOutlined />} onClick={sendMessage}>
|
||||
Send
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Popover
|
||||
content={<EmojiPicker onEmojiSelect={handleEmojiSelect} />}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.streamInfo {
|
||||
.root {
|
||||
position: relative;
|
||||
display: grid;
|
||||
}
|
||||
@ -34,3 +34,44 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
&.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0 .3rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.mobileInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.mobileStatus {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
.viewerCount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.liveStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: .5rem;
|
||||
font-size: .8rem;
|
||||
gap: 4px;
|
||||
.liveCircle {
|
||||
border-radius: 50%;
|
||||
background-color: red;
|
||||
width: .5rem;
|
||||
height: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,16 +1,56 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import cn from 'classnames';
|
||||
import { EyeFilled } from '@ant-design/icons';
|
||||
import { useEffect } from 'react';
|
||||
import { ClientConfig } from '../../../interfaces/client-config.model';
|
||||
import { clientConfigStateAtom } from '../../stores/ClientConfigStore';
|
||||
import {
|
||||
clientConfigStateAtom,
|
||||
isOnlineSelector,
|
||||
serverStatusState,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
import { ServerLogo } from '../../ui';
|
||||
import StatusBar from '../../ui/Statusbar';
|
||||
import CategoryIcon from '../../ui/CategoryIcon/CategoryIcon';
|
||||
import SocialLinks from '../../ui/SocialLinks/SocialLinks';
|
||||
import s from './StreamInfo.module.scss';
|
||||
import { ServerStatus } from '../../../interfaces/server-status.model';
|
||||
|
||||
export default function StreamInfo() {
|
||||
interface Props {
|
||||
isMobile: boolean;
|
||||
}
|
||||
export default function StreamInfo({ isMobile }: Props) {
|
||||
const { socialHandles, name, title, tags } = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const { viewerCount, lastConnectTime, lastDisconnectTime } =
|
||||
useRecoilValue<ServerStatus>(serverStatusState);
|
||||
const online = useRecoilValue<boolean>(isOnlineSelector);
|
||||
|
||||
return (
|
||||
<div className={s.streamInfo}>
|
||||
useEffect(() => {
|
||||
console.log({ online });
|
||||
}, [online]);
|
||||
|
||||
return isMobile ? (
|
||||
<div className={cn(s.root, s.mobile)}>
|
||||
<div className={s.mobileInfo}>
|
||||
<ServerLogo src="/logo" />
|
||||
<div className={s.title}>{name}</div>
|
||||
</div>
|
||||
<div className={s.mobileStatus}>
|
||||
<div className={s.viewerCount}>
|
||||
{online && (
|
||||
<>
|
||||
<span>{viewerCount}</span>
|
||||
<EyeFilled />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={s.liveStatus}>
|
||||
{online && <div className={s.liveCircle} />}
|
||||
<span>{online ? 'LIVE' : 'OFFLINE'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={s.root}>
|
||||
<div className={s.logoTitleSection}>
|
||||
<ServerLogo src="/logo" />
|
||||
<div className={s.titleSection}>
|
||||
@ -23,6 +63,12 @@ export default function StreamInfo() {
|
||||
<SocialLinks links={socialHandles} />
|
||||
</div>
|
||||
</div>
|
||||
<StatusBar
|
||||
online={online}
|
||||
lastConnectTime={lastConnectTime}
|
||||
lastDisconnectTime={lastDisconnectTime}
|
||||
viewerCount={viewerCount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,32 +1,8 @@
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
&.mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobileChat {
|
||||
position: relative;
|
||||
display: block;
|
||||
// top: 0px;
|
||||
width: 100%;
|
||||
[data-virtuoso-scroller] {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.leftCol {
|
||||
display: flex;
|
||||
@ -40,3 +16,26 @@
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
&.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 64px);
|
||||
overflow: hidden;
|
||||
.topHalf {
|
||||
display: grid;
|
||||
grid-template-rows: 30vh 5vh 5vh;
|
||||
height: 40vh;
|
||||
// overflow: hidden;
|
||||
}
|
||||
.lowerHalf {
|
||||
height: 60vh;
|
||||
}
|
||||
}
|
||||
.mobileChat {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import {
|
||||
chatDisplayNameAtom,
|
||||
chatUserIdAtom,
|
||||
isChatVisibleSelector,
|
||||
serverStatusState,
|
||||
appStateAtom,
|
||||
isOnlineSelector,
|
||||
isMobileAtom,
|
||||
@ -28,8 +27,6 @@ import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import ChatTextField from '../../chat/ChatTextField/ChatTextField';
|
||||
import ActionButtonRow from '../../action-buttons/ActionButtonRow';
|
||||
import ActionButton from '../../action-buttons/ActionButton';
|
||||
import Statusbar from '../Statusbar/Statusbar';
|
||||
import { ServerStatus } from '../../../interfaces/server-status.model';
|
||||
import { Follower } from '../../../interfaces/follower';
|
||||
import NotifyReminderPopup from '../NotifyReminderPopup/NotifyReminderPopup';
|
||||
import OfflineBanner from '../OfflineBanner/OfflineBanner';
|
||||
@ -38,13 +35,13 @@ import FollowButton from '../../action-buttons/FollowButton';
|
||||
import NotifyButton from '../../action-buttons/NotifyButton';
|
||||
import Modal from '../Modal/Modal';
|
||||
import BrowserNotifyModal from '../../modals/BrowserNotify/BrowserNotifyModal';
|
||||
import StreamInfo from '../../common/StreamInfo';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const { Content } = Layout;
|
||||
|
||||
export default function ContentComponent() {
|
||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||
const status = useRecoilValue<ServerStatus>(serverStatusState);
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector);
|
||||
const [isMobile, setIsMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
|
||||
@ -54,7 +51,6 @@ export default function ContentComponent() {
|
||||
const chatUserId = useRecoilValue<string>(chatUserIdAtom);
|
||||
|
||||
const { extraPageContent, version, name, summary } = clientConfig;
|
||||
const { viewerCount, lastConnectTime, lastDisconnectTime } = status;
|
||||
const [showNotifyReminder, setShowNotifyReminder] = useState(false);
|
||||
const [showNotifyPopup, setShowNotifyPopup] = useState(false);
|
||||
|
||||
@ -129,13 +125,6 @@ export default function ContentComponent() {
|
||||
text="Stream is offline text goes here. Will create a new form to set it in the Admin."
|
||||
/>
|
||||
)}
|
||||
|
||||
<Statusbar
|
||||
online={online}
|
||||
lastConnectTime={lastConnectTime}
|
||||
lastDisconnectTime={lastDisconnectTime}
|
||||
viewerCount={viewerCount}
|
||||
/>
|
||||
<div className={s.buttonsLogoTitleSection}>
|
||||
<ActionButtonRow>
|
||||
{externalActionButtons}
|
||||
@ -158,12 +147,18 @@ export default function ContentComponent() {
|
||||
<BrowserNotifyModal />
|
||||
</Modal>
|
||||
</div>
|
||||
<StreamInfo isMobile={isMobile} />
|
||||
</div>
|
||||
<div className={s.lowerHalf}>
|
||||
<Tabs defaultActiveKey="0">
|
||||
{isChatVisible && isMobile && (
|
||||
<TabPane tab="Chat" key="0" className={s.pageContentSection}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<TabPane
|
||||
tab="Chat"
|
||||
key="0"
|
||||
className={s.pageContentSection}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<div style={{ position: 'relative', height: '100%' }}>
|
||||
<div className={s.mobileChat}>
|
||||
<ChatContainer
|
||||
messages={messages}
|
||||
@ -173,8 +168,8 @@ export default function ContentComponent() {
|
||||
isModerator={false}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<ChatTextField />
|
||||
</div>
|
||||
<ChatTextField />
|
||||
</div>
|
||||
</TabPane>
|
||||
)}
|
||||
@ -186,7 +181,7 @@ export default function ContentComponent() {
|
||||
<FollowerCollection total={total} followers={followers} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<Footer version={version} />
|
||||
{!isMobile && <Footer version={version} />}
|
||||
</div>
|
||||
</div>
|
||||
{isChatVisible && !isMobile && <Sidebar />}
|
||||
|
||||
@ -5,15 +5,22 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 20;
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.4rem .7rem;
|
||||
background-color: var(--default-bg-color);
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
span {
|
||||
margin-left: 1rem;
|
||||
font-size: 1.7rem;
|
||||
margin-left: .5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
line-height: 5vh;
|
||||
height: 5vh;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,9 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
width: clamp(4rem, 10vw, 120px);
|
||||
height: clamp(4rem, 10vw, 120px);
|
||||
margin-right: .5rem;
|
||||
width: clamp(2.5vh, 9vw, 120px);
|
||||
height: clamp(2.5vh, 9vw, 120px);
|
||||
border-radius: 50%;
|
||||
border-width: 3px;
|
||||
border-style: solid;
|
||||
@ -27,3 +28,4 @@
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@ -7,9 +7,8 @@
|
||||
width: clamp(200px, 100%, 300px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
;
|
||||
background-color: var(--theme-background-secondary);
|
||||
margin: auto;
|
||||
margin: 1rem auto;
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
1
web/components/ui/Statusbar/index.ts
Normal file
1
web/components/ui/Statusbar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Statusbar';
|
||||
@ -4,3 +4,4 @@ export { default as Footer } from './Footer/index';
|
||||
export { default as Content } from './Content/index';
|
||||
export { default as ModIcon } from './ModIcon';
|
||||
export { default as ServerLogo } from './Logo';
|
||||
export { default as StatusBar } from './Statusbar';
|
||||
|
||||
@ -10,3 +10,9 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.player {
|
||||
height: 30vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,11 +9,17 @@ body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: var(--theme-font-family), var(--theme-header-font-family), sans-serif;
|
||||
font-size: clamp(14px, 1.5vw, 17px);
|
||||
font-size: clamp(14px, 1vw, 17px);
|
||||
background-color: var(--default-bg-color);
|
||||
color: var(--default-text-color);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// a {
|
||||
// color: inherit;
|
||||
// text-decoration: none;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user