Refactor Toolbar, move video & notifications

This commit is contained in:
Jerko Steiner 2019-11-17 23:51:54 -03:00
parent 9d68e4c1f4
commit b88889665f
10 changed files with 134 additions and 110 deletions

View File

@ -107,10 +107,7 @@ export function setMediaVisible(visible: boolean): MediaVisibleAction {
export const play = makeAction('MEDIA_PLAY', async () => { export const play = makeAction('MEDIA_PLAY', async () => {
const promises = Array const promises = Array
.from(document.querySelectorAll('video')) .from(document.querySelectorAll('video'))
.filter(video => { .filter(video => video.paused)
console.log('video', video.paused, video)
return video.paused
})
.map(video => video.play()) .map(video => video.play())
await Promise.all(promises) await Promise.all(promises)
}) })

View File

@ -75,9 +75,13 @@ export default class App extends React.PureComponent<AppProps, AppState> {
const { videos } = this.state const { videos } = this.state
const chatVisibleClassName = classnames({
'chat-visible': this.state.chatVisible,
})
return ( return (
<div className="app"> <div className="app">
<Side align='end' left zIndex={1}> <Side align='end' left zIndex={2}>
<Toolbar <Toolbar
chatVisible={this.state.chatVisible} chatVisible={this.state.chatVisible}
messagesCount={messagesCount} messagesCount={messagesCount}
@ -86,7 +90,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
stream={streams[constants.ME]} stream={streams[constants.ME]}
/> />
</Side> </Side>
<Side top zIndex={2}> <Side className={chatVisibleClassName} top zIndex={1}>
<Notifications <Notifications
dismiss={dismissNotification} dismiss={dismissNotification}
notifications={notifications} notifications={notifications}
@ -99,11 +103,7 @@ export default class App extends React.PureComponent<AppProps, AppState> {
sendMessage={sendMessage} sendMessage={sendMessage}
visible={this.state.chatVisible} visible={this.state.chatVisible}
/> />
<div <div className={classnames('videos', chatVisibleClassName)}>
className={classnames('videos', {
'chat-visible': this.state.chatVisible,
})}
>
{streams[constants.ME] && ( {streams[constants.ME] && (
<Video <Video
videos={videos} videos={videos}

View File

@ -123,7 +123,6 @@ export const Media = c(React.memo(function Media(props: MediaProps) {
)} )}
</Alerts> </Alerts>
{props.autoplayError && <AutoplayMessage play={props.play} />}
<MediaForm {...props} /> <MediaForm {...props} />
</div> </div>
) )

View File

@ -7,6 +7,7 @@ export type Top = { top: true }
export type Bottom = { bottom: true } export type Bottom = { bottom: true }
export type SideProps = (Left | Right | Top | Bottom) & { export type SideProps = (Left | Right | Top | Bottom) & {
className?: string
zIndex: number zIndex: number
children: React.ReactNode children: React.ReactNode
align?: 'baseline' | 'center' | 'end' align?: 'baseline' | 'center' | 'end'
@ -14,11 +15,11 @@ export type SideProps = (Left | Right | Top | Bottom) & {
export const Side = React.memo( export const Side = React.memo(
function Side(props: SideProps) { function Side(props: SideProps) {
const className = classnames('side', { ...props }) const { className, zIndex, ...otherProps } = props
return ( return (
<div <div
className={className} className={classnames('side', className, { ...otherProps })}
style={{alignItems: props.align || 'center', zIndex: props.zIndex}} style={{alignItems: props.align || 'center', zIndex }}
> >
{props.children} {props.children}
</div> </div>

View File

@ -22,6 +22,43 @@ export interface ToolbarState {
fullScreenEnabled: boolean fullScreenEnabled: boolean
} }
export interface ToolbarButtonProps {
className?: string
badge?: string | number
blink?: boolean
onClick: () => void
icon: string
offIcon?: string
on?: boolean
title: string
}
function ToolbarButton(props: ToolbarButtonProps) {
const { blink, on } = props
const icon = !on && props.offIcon ? props.offIcon : props.icon
function onClick(event: React.MouseEvent<HTMLElement>) {
props.onClick()
document.activeElement &&
document.activeElement instanceof HTMLElement &&
document.activeElement.blur()
}
return (
<a
className={classnames('button', props.className, { blink, on })}
onClick={onClick}
href='#'
>
<span className={classnames('icon', icon)}>
{!!props.badge && <span className='badge'>{props.badge}</span>}
</span>
<span className="tooltip">{props.title}</span>
</a>
)
}
export default class Toolbar export default class Toolbar
extends React.PureComponent<ToolbarProps, ToolbarState> { extends React.PureComponent<ToolbarProps, ToolbarState> {
file = React.createRef<HTMLInputElement>() file = React.createRef<HTMLInputElement>()
@ -61,7 +98,7 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
screenfull.toggle() screenfull.toggle()
this.setState({ this.setState({
...this.state, ...this.state,
fullScreenEnabled: !this.state.fullScreenEnabled, fullScreenEnabled: !screenfull.isFullscreen,
}) })
} }
} }
@ -84,88 +121,73 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
} }
render () { render () {
const { messagesCount, stream } = this.props const { messagesCount, stream } = this.props
const unreadCount = messagesCount - this.state.readMessages
const hasUnread = unreadCount > 0
return ( return (
<div className="toolbar active"> <div className="toolbar active">
<a onClick={this.handleToggleChat} <input
className={classnames('button chat', { style={hidden}
on: this.props.chatVisible, type="file"
})} multiple
href='#' ref={this.file}
data-blink={!this.props.chatVisible && onChange={this.handleSelectFiles}
messagesCount > this.state.readMessages} />
title="Chat"
> <ToolbarButton
<span className="icon icon-question_answer" /> badge={unreadCount}
<span className="tooltip">Toggle Chat</span> className='chat'
</a> icon='icon-question_answer'
<a blink={!this.props.chatVisible && hasUnread}
className="button send-file" onClick={this.handleToggleChat}
on={this.props.chatVisible}
title='Toggle Chat'
/>
<ToolbarButton
className='send-file'
icon='icon-file-text2'
onClick={this.handleSendFile} onClick={this.handleSendFile}
title="Send file" title='Send File'
href='#' />
>
<input
style={hidden}
type="file"
multiple
ref={this.file}
onChange={this.handleSelectFiles}
/>
<span className="icon icon-file-text2" />
<span className="tooltip">Send File</span>
</a>
{stream && ( {stream && (
<React.Fragment> <React.Fragment>
<a <ToolbarButton
onClick={this.handleMicClick} onClick={this.handleMicClick}
className={classnames('button mute-audio', { className='mute-audio'
on: this.state.micMuted, on={this.state.micMuted}
})} icon='icon-mic_off'
href='#' offIcon='icon-mic'
title="Mute audio" title='Toggle Microphone'
> />
<span className="on icon icon-mic_off" /> <ToolbarButton
<span className="off icon icon-mic" /> onClick={this.handleCamClick}
<span className="tooltip">Toggle Microphone</span> className='mute-video'
</a> on={this.state.camDisabled}
<a onClick={this.handleCamClick} icon='icon-videocam_off'
className={classnames('button mute-video', { offIcon='icon-videocam'
on: this.state.camDisabled, title='Toggle Camera'
})} />
href='#'
title="Mute video"
>
<span className="on icon icon-videocam_off" />
<span className="off icon icon-videocam" />
<span className="tooltip">Toggle Camera</span>
</a>
</React.Fragment> </React.Fragment>
)} )}
<a <ToolbarButton
onClick={this.handleFullscreenClick} onClick={this.handleFullscreenClick}
href='#' className='fullscreen'
className={classnames('button fullscreen', { icon='icon-fullscreen_exit'
on: this.state.fullScreenEnabled, offIcon='icon-fullscreen'
})} on={this.state.fullScreenEnabled}
title="Enter fullscreen" title='Toggle Fullscreen'
> />
<span className="on icon icon-fullscreen_exit" />
<span className="off icon icon-fullscreen" />
<span className="tooltip">Fullscreen</span>
</a>
<a <ToolbarButton
onClick={this.handleHangoutClick} onClick={this.handleHangoutClick}
className="button hangup" className='hangup'
href='#' icon='icon-call_end'
title="Hang Up" title="Hang Up"
> />
<span className="icon icon-call_end" />
<span className="tooltip">Hang Up</span>
</a>
</div> </div>
) )
} }

View File

@ -52,10 +52,7 @@ export default class Video extends React.PureComponent<VideoProps> {
id={`video-${socket.id}`} id={`video-${socket.id}`}
autoPlay autoPlay
onClick={this.handleClick} onClick={this.handleClick}
onLoadedMetadata={() => { onLoadedMetadata={() => this.props.play()}
console.log('onLoadedMetadata')
this.props.play()
}}
playsInline playsInline
ref={this.videoRef} ref={this.videoRef}
muted={muted} muted={muted}

View File

@ -28,6 +28,15 @@
border-radius: 4px; border-radius: 4px;
border-bottom: 2px solid darken(#fff, 10%); border-bottom: 2px solid darken(#fff, 10%);
text-align: center; text-align: center;
color: black;
background-color: white;
cursor: pointer;
&:hover {
background-color: darken(white, 5%);
}
&:active {
background-color: darken(white, 10%);
}
} }
select, button { select, button {

View File

@ -9,6 +9,7 @@
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 48px; border-radius: 48px;
background-color: rgba(0, 0, 0, 0.2);
box-shadow: 2px 2px 24px #444; box-shadow: 2px 2px 24px #444;
transition: all .1s; transition: all .1s;
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
@ -21,8 +22,13 @@
top: 12px; top: 12px;
} }
&.on { .badge {
display: none; font-family: sans-serif;
font-weight: bold;
color: white;
position: relative;
font-size: 12px;
top: 23px;
} }
} }
@ -35,17 +41,18 @@
text-shadow: 0 0 5px black; text-shadow: 0 0 5px black;
transition: opacity 200ms ease-in 25ms, transform 100ms ease-in; transition: opacity 200ms ease-in 25ms, transform 100ms ease-in;
transform: translateX(-100%); transform: translateX(-100%);
z-index: 0; pointer-events: none;
} }
.button { .button {
outline: none;
text-decoration: none; text-decoration: none;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
&:hover { &:hover, &:focus {
.icon { .icon {
box-shadow: 4px 4px 48px #666; box-shadow: 4px 4px 48px #666;
cursor: pointer; cursor: pointer;
@ -62,12 +69,6 @@
} }
&.on .icon { &.on .icon {
background: lighten(#407cf7, 10%); background: lighten(#407cf7, 10%);
&.on {
display: inherit;
}
&.off {
display: none;
}
} }
} }
@ -75,12 +76,10 @@
margin-top: 1rem; margin-top: 1rem;
} }
.chat { .chat.blink .icon {
&[data-blink="true"] .icon { -webkit-animation: bg-blink 1s infinite;
-webkit-animation: bg-blink 1s infinite; -moz-animation: bg-blink 1s infinite;
-moz-animation: bg-blink 1s infinite; animation: bg-blink 1s infinite;
animation: bg-blink 1s infinite;
}
} }
} }

View File

@ -2,8 +2,9 @@
position: fixed; position: fixed;
height: 100px; height: 100px;
bottom: 1rem; bottom: 1rem;
right: 1rem; right: 0;
text-align: right; text-align: right;
transition: right cubic-bezier(0.55, 0, 0, 1) 500ms;
$video-size: 100px; $video-size: 100px;
@ -15,6 +16,7 @@
width: $video-size; width: $video-size;
height: 100%; height: 100%;
z-index: 3; z-index: 3;
margin-right: 1rem;
video { video {
border-radius: 10px; border-radius: 10px;
@ -25,10 +27,6 @@
} }
} }
.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;
@ -40,6 +38,7 @@
top: 0; top: 0;
bottom: 0; bottom: 0;
z-index: -1; z-index: -1;
transform: none;
video { video {
border-radius: 0; border-radius: 0;

View File

@ -165,6 +165,7 @@ body.call {
.side { .side {
position: absolute; position: absolute;
display: flex; display: flex;
transition: right cubic-bezier(0.55, 0, 0, 1) 500ms;
&.left { &.left {
flex-direction: row; flex-direction: row;
@ -195,6 +196,6 @@ body.call {
} }
} }
.chat-visible { .app .chat-visible {
transform: translateX(-330px); right: 320px;
} }