Refactor Toolbar, move video & notifications
This commit is contained in:
parent
9d68e4c1f4
commit
b88889665f
@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user