Use <Side /> instead of position absolute
Some components still use position fixed. This could change in the future.
This commit is contained in:
parent
58039eb086
commit
fddb88f5b8
@ -1,16 +1,18 @@
|
|||||||
|
import classnames from 'classnames'
|
||||||
import map from 'lodash/map'
|
import map from 'lodash/map'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Peer from 'simple-peer'
|
import Peer from 'simple-peer'
|
||||||
import { Message } from '../actions/ChatActions'
|
import { Message } from '../actions/ChatActions'
|
||||||
import { Notification, dismissNotification } from '../actions/NotifyActions'
|
import { dismissNotification, Notification } from '../actions/NotifyActions'
|
||||||
import { TextMessage } from '../actions/PeerActions'
|
import { TextMessage } from '../actions/PeerActions'
|
||||||
import { AddStreamPayload } from '../actions/StreamActions'
|
import { AddStreamPayload } from '../actions/StreamActions'
|
||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
import Chat from './Chat'
|
import Chat from './Chat'
|
||||||
|
import { Media } from './Media'
|
||||||
import Notifications from './Notifications'
|
import Notifications from './Notifications'
|
||||||
|
import { Side } from './Side'
|
||||||
import Toolbar from './Toolbar'
|
import Toolbar from './Toolbar'
|
||||||
import Video from './Video'
|
import Video from './Video'
|
||||||
import { Media } from './Media'
|
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
active: string | null
|
active: string | null
|
||||||
@ -75,47 +77,61 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Toolbar
|
<Side align='end' left zIndex={1}>
|
||||||
chatVisible={this.state.chatVisible}
|
<Toolbar
|
||||||
messagesCount={messagesCount}
|
chatVisible={this.state.chatVisible}
|
||||||
onToggleChat={this.handleToggleChat}
|
messagesCount={messagesCount}
|
||||||
onSendFile={onSendFile}
|
onToggleChat={this.handleToggleChat}
|
||||||
stream={streams[constants.ME]}
|
onSendFile={onSendFile}
|
||||||
/>
|
stream={streams[constants.ME]}
|
||||||
|
/>
|
||||||
|
</Side>
|
||||||
|
<Side top zIndex={2}>
|
||||||
<Notifications
|
<Notifications
|
||||||
dismiss={dismissNotification}
|
dismiss={dismissNotification}
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
/>
|
/>
|
||||||
<Media />
|
<Media />
|
||||||
|
</Side>
|
||||||
<Chat
|
<Chat
|
||||||
messages={messages}
|
messages={messages}
|
||||||
onClose={this.handleHideChat}
|
onClose={this.handleHideChat}
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
visible={this.state.chatVisible}
|
visible={this.state.chatVisible}
|
||||||
/>
|
/>
|
||||||
<div className="videos">
|
<div
|
||||||
<Video
|
className={classnames('videos', {
|
||||||
videos={videos}
|
'chat-visible': this.state.chatVisible,
|
||||||
active={active === constants.ME}
|
})}
|
||||||
onClick={toggleActive}
|
>
|
||||||
play={play}
|
{streams[constants.ME] && (
|
||||||
stream={streams[constants.ME]}
|
|
||||||
userId={constants.ME}
|
|
||||||
muted
|
|
||||||
mirrored
|
|
||||||
/>
|
|
||||||
|
|
||||||
{map(peers, (_, userId) => (
|
|
||||||
<Video
|
<Video
|
||||||
active={userId === active}
|
videos={videos}
|
||||||
key={userId}
|
active={active === constants.ME}
|
||||||
onClick={toggleActive}
|
onClick={toggleActive}
|
||||||
play={play}
|
play={play}
|
||||||
stream={streams[userId]}
|
stream={streams[constants.ME]}
|
||||||
userId={userId}
|
userId={constants.ME}
|
||||||
videos={videos}
|
muted
|
||||||
|
mirrored
|
||||||
/>
|
/>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
|
{
|
||||||
|
map(peers, (_, userId) => userId)
|
||||||
|
.filter(stream => !!stream)
|
||||||
|
.map(userId =>
|
||||||
|
<Video
|
||||||
|
active={userId === active}
|
||||||
|
key={userId}
|
||||||
|
onClick={toggleActive}
|
||||||
|
play={play}
|
||||||
|
stream={streams[userId]}
|
||||||
|
userId={userId}
|
||||||
|
videos={videos}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -101,7 +101,7 @@ export const AutoplayMessage = React.memo(
|
|||||||
function Autoplay(props: AutoplayProps) {
|
function Autoplay(props: AutoplayProps) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
The browser has blocked video autoplay on this page.
|
Your browser has blocked video autoplay on this page.
|
||||||
To continue with your call, please press the play button:
|
To continue with your call, please press the play button:
|
||||||
|
|
||||||
<button className='button' onClick={props.play}>
|
<button className='button' onClick={props.play}>
|
||||||
|
|||||||
@ -42,7 +42,7 @@ const Notification = React.memo(
|
|||||||
export default class Notifications
|
export default class Notifications
|
||||||
extends React.PureComponent<NotificationsProps> {
|
extends React.PureComponent<NotificationsProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
max: 10,
|
max: 5,
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
const { dismiss, notifications, max } = this.props
|
const { dismiss, notifications, max } = this.props
|
||||||
|
|||||||
27
src/client/components/Side.tsx
Normal file
27
src/client/components/Side.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
export type Left = { left: true }
|
||||||
|
export type Right = { right: true }
|
||||||
|
export type Top = { top: true }
|
||||||
|
export type Bottom = { bottom: true }
|
||||||
|
|
||||||
|
export type SideProps = (Left | Right | Top | Bottom) & {
|
||||||
|
zIndex: number
|
||||||
|
children: React.ReactNode
|
||||||
|
align?: 'baseline' | 'center' | 'end'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Side = React.memo(
|
||||||
|
function Side(props: SideProps) {
|
||||||
|
const className = classnames('side', { ...props })
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{alignItems: props.align || 'center', zIndex: props.zIndex}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@ -87,7 +87,7 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="toolbar active">
|
<div className="toolbar active">
|
||||||
<div onClick={this.handleToggleChat}
|
<a onClick={this.handleToggleChat}
|
||||||
className={classnames('button chat', {
|
className={classnames('button chat', {
|
||||||
on: this.props.chatVisible,
|
on: this.props.chatVisible,
|
||||||
})}
|
})}
|
||||||
@ -96,8 +96,8 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
title="Chat"
|
title="Chat"
|
||||||
>
|
>
|
||||||
<span className="icon icon-question_answer" />
|
<span className="icon icon-question_answer" />
|
||||||
</div>
|
</a>
|
||||||
<div
|
<a
|
||||||
className="button send-file"
|
className="button send-file"
|
||||||
onClick={this.handleSendFile}
|
onClick={this.handleSendFile}
|
||||||
title="Send file"
|
title="Send file"
|
||||||
@ -110,11 +110,11 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
onChange={this.handleSelectFiles}
|
onChange={this.handleSelectFiles}
|
||||||
/>
|
/>
|
||||||
<span className="icon icon-file-text2" />
|
<span className="icon icon-file-text2" />
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
{stream && (
|
{stream && (
|
||||||
<div>
|
<React.Fragment>
|
||||||
<div
|
<a
|
||||||
onClick={this.handleMicClick}
|
onClick={this.handleMicClick}
|
||||||
className={classnames('button mute-audio', {
|
className={classnames('button mute-audio', {
|
||||||
on: this.state.micMuted,
|
on: this.state.micMuted,
|
||||||
@ -123,8 +123,8 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
>
|
>
|
||||||
<span className="on icon icon-mic_off" />
|
<span className="on icon icon-mic_off" />
|
||||||
<span className="off icon icon-mic" />
|
<span className="off icon icon-mic" />
|
||||||
</div>
|
</a>
|
||||||
<div onClick={this.handleCamClick}
|
<a onClick={this.handleCamClick}
|
||||||
className={classnames('button mute-video', {
|
className={classnames('button mute-video', {
|
||||||
on: this.state.camDisabled,
|
on: this.state.camDisabled,
|
||||||
})}
|
})}
|
||||||
@ -132,11 +132,11 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
>
|
>
|
||||||
<span className="on icon icon-videocam_off" />
|
<span className="on icon icon-videocam_off" />
|
||||||
<span className="off icon icon-videocam" />
|
<span className="off icon icon-videocam" />
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div onClick={this.handleFullscreenClick}
|
<a onClick={this.handleFullscreenClick}
|
||||||
className={classnames('button fullscreen', {
|
className={classnames('button fullscreen', {
|
||||||
on: this.state.fullScreenEnabled,
|
on: this.state.fullScreenEnabled,
|
||||||
})}
|
})}
|
||||||
@ -144,14 +144,14 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
>
|
>
|
||||||
<span className="on icon icon-fullscreen_exit" />
|
<span className="on icon icon-fullscreen_exit" />
|
||||||
<span className="off icon icon-fullscreen" />
|
<span className="off icon icon-fullscreen" />
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
<div onClick={this.handleHangoutClick}
|
<a onClick={this.handleHangoutClick}
|
||||||
className="button hangup"
|
className="button hangup"
|
||||||
title="Hangup"
|
title="Hangup"
|
||||||
>
|
>
|
||||||
<span className="icon icon-call_end" />
|
<span className="icon icon-call_end" />
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,11 +64,26 @@ describe('App', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('chat toggle', () => {
|
||||||
|
it('toggles chat state', async () => {
|
||||||
|
await render()
|
||||||
|
const chatButton = node.querySelector('.toolbar .button.chat')!
|
||||||
|
expect(chatButton).toBeTruthy()
|
||||||
|
TestUtils.Simulate.click(chatButton)
|
||||||
|
TestUtils.Simulate.click(chatButton)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('state', () => {
|
describe('state', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
state.streams = {
|
state.streams = {
|
||||||
test: {
|
[constants.ME]: {
|
||||||
userId: 'test',
|
userId: constants.ME,
|
||||||
|
stream: new MediaStream(),
|
||||||
|
url: 'blob://',
|
||||||
|
},
|
||||||
|
'other-user': {
|
||||||
|
userId: 'other-user',
|
||||||
stream: new MediaStream(),
|
stream: new MediaStream(),
|
||||||
url: 'blob://',
|
url: 'blob://',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
.alert {
|
.alert {
|
||||||
background-color: black;
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
top: 0;
|
|
||||||
transition: visibility 100ms ease-in, opacity 100ms ease-in;
|
transition: visibility 100ms ease-in, opacity 100ms ease-in;
|
||||||
z-index: 4;
|
color: $color-warning;
|
||||||
pointer-events: none;
|
text-shadow: 0px 0px 3px black;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -18,12 +16,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
line-height: 1.4rem;
|
@include button($color-primary, $color-warning);
|
||||||
border: none;
|
font-size: 1.2rem;
|
||||||
border-radius: 0.3rem;
|
padding: 1rem;
|
||||||
color: $color-info;
|
|
||||||
background-color: $color-fg;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
transition: transform 0.5s cubic-bezier(0.55, 0, 0, 1), box-shadow 0.5s cubic-bezier(0.55, 0, 0, 1);
|
transition: transform 0.5s cubic-bezier(0.55, 0, 0, 1), box-shadow 0.5s cubic-bezier(0.55, 0, 0, 1);
|
||||||
width: 320px;
|
width: 320px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
z-index: 3;
|
z-index: 4;
|
||||||
|
|
||||||
&.show {
|
&.show {
|
||||||
-ms-transform: none;
|
-ms-transform: none;
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
.media-container form.media {
|
.media-container form.media {
|
||||||
|
margin: 1rem auto 0;
|
||||||
max-width: 440px;
|
max-width: 440px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
left: 50%;
|
|
||||||
top: 100px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
margin: 0.25rem;
|
margin: 0.25rem;
|
||||||
|
|||||||
@ -2,17 +2,15 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-family: $font-monospace;
|
font-family: $font-monospace;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
left: 1rem;
|
|
||||||
position: fixed;
|
|
||||||
right: 1rem;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
top: 1rem;
|
width: 100%;
|
||||||
z-index: 3;
|
padding: 1rem;
|
||||||
|
height: 100px;
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
color: $color-info;
|
color: $color-info;
|
||||||
|
text-shadow: 0px 0px 3px black;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification.error {
|
.notification.error {
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
bottom: 20px;
|
margin: 0 0 1rem 1rem;
|
||||||
left: 6vw;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 3;
|
|
||||||
|
|
||||||
/* on icons are hidden by default */
|
/* on icons are hidden by default */
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@ -38,7 +34,6 @@
|
|||||||
border-radius: 48px;
|
border-radius: 48px;
|
||||||
box-shadow: 2px 2px 24px #444;
|
box-shadow: 2px 2px 24px #444;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 0 3vh 0;
|
|
||||||
transform: translateX(calc(-6vw - 96px));
|
transform: translateX(calc(-6vw - 96px));
|
||||||
transition: all .1s;
|
transition: all .1s;
|
||||||
transition-timing-function: ease-in-out;
|
transition-timing-function: ease-in-out;
|
||||||
@ -48,6 +43,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button + .button {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* off icons are hidden when parent svg has class 'on' */
|
/* off icons are hidden when parent svg has class 'on' */
|
||||||
|
|
||||||
&.active .button {
|
&.active .button {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
.videos {
|
.videos {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
bottom: 15px;
|
bottom: 1rem;
|
||||||
right: 0px;
|
right: 1rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
$video-size: 100px;
|
$video-size: 100px;
|
||||||
@ -12,10 +12,9 @@
|
|||||||
box-shadow: 0px 0px 5px black;
|
box-shadow: 0px 0px 5px black;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 10px;
|
|
||||||
width: $video-size;
|
width: $video-size;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 2;
|
z-index: 3;
|
||||||
|
|
||||||
video {
|
video {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@ -26,6 +25,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-container + .video-container {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.video-container.active {
|
.video-container.active {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|||||||
@ -161,3 +161,40 @@ body.call {
|
|||||||
opacity: 0.01;
|
opacity: 0.01;
|
||||||
transition: opacity 100ms ease-in;
|
transition: opacity 100ms ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
flex-direction: row;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
flex-direction: row;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top {
|
||||||
|
flex-direction: column;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-visible {
|
||||||
|
transform: translateX(-330px);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user