Fix toolbar icons. Hangup removes video stream

This commit is contained in:
Jerko Steiner 2019-11-18 09:35:37 -03:00
parent a097e26a20
commit c89886bbfa
12 changed files with 72 additions and 58 deletions

View File

@ -1,5 +1,8 @@
import { makeAction, AsyncAction } from '../async' import { makeAction, AsyncAction } from '../async'
import { MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_ENUMERATE, MEDIA_STREAM, MEDIA_VISIBLE_SET } from '../constants' import { MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_ENUMERATE, MEDIA_STREAM } from '../constants'
import _debug from 'debug'
const debug = _debug('peercalls')
export interface MediaDevice { export interface MediaDevice {
id: string id: string
@ -91,19 +94,6 @@ export function setAudioConstraint(
} }
} }
export interface MediaVisibleAction {
type: 'MEDIA_VISIBLE_SET'
payload: { visible: boolean }
}
export function setMediaVisible(visible: boolean): MediaVisibleAction {
return {
type: MEDIA_VISIBLE_SET,
payload: { visible },
}
}
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'))
@ -114,7 +104,10 @@ export const play = makeAction('MEDIA_PLAY', async () => {
export const getMediaStream = makeAction( export const getMediaStream = makeAction(
MEDIA_STREAM, MEDIA_STREAM,
async (constraints: GetMediaConstraints) => getUserMedia(constraints), async (constraints: GetMediaConstraints) => {
debug('getMediaStream', constraints)
return getUserMedia(constraints)
},
) )
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]> export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
@ -126,5 +119,4 @@ export type MediaAction =
MediaAudioConstraintAction | MediaAudioConstraintAction |
MediaEnumerateAction | MediaEnumerateAction |
MediaStreamAction | MediaStreamAction |
MediaVisibleAction |
MediaPlayAction MediaPlayAction

View File

@ -1,7 +1,5 @@
import uniqueId from 'lodash/uniqueId' import uniqueId from 'lodash/uniqueId'
import { Dispatch } from 'redux'
import * as constants from '../constants' import * as constants from '../constants'
import { ThunkResult } from '../store'
function format (string: string, args: string[]) { function format (string: string, args: string[]) {
string = args string = args
@ -11,7 +9,7 @@ function format (string: string, args: string[]) {
export type NotifyType = 'info' | 'warning' | 'error' export type NotifyType = 'info' | 'warning' | 'error'
function notify(dispatch: Dispatch, type: NotifyType, args: string[]) { function notify(type: NotifyType, args: string[]) {
const string = args[0] || '' const string = args[0] || ''
const message = format(string, Array.prototype.slice.call(args, 1)) const message = format(string, Array.prototype.slice.call(args, 1))
const id = uniqueId('notification') const id = uniqueId('notification')
@ -20,16 +18,16 @@ function notify(dispatch: Dispatch, type: NotifyType, args: string[]) {
return addNotification(payload) return addNotification(payload)
} }
export const info = (...args: any[]): ThunkResult<NotificationAddAction> => { export const info = (...args: any[]): NotificationAddAction => {
return dispatch => dispatch(notify(dispatch, 'info', args)) return notify('info', args)
} }
export const warning = (...args: any[]): ThunkResult<NotificationAddAction> => { export const warning = (...args: any[]): NotificationAddAction => {
return dispatch => dispatch(notify(dispatch, 'warning', args)) return notify('warning', args)
} }
export const error = (...args: any[]): ThunkResult<NotificationAddAction> => { export const error = (...args: any[]): NotificationAddAction => {
return dispatch => dispatch(notify(dispatch, 'error', args)) return notify('error', args)
} }
function addNotification(payload: Notification): NotificationAddAction { function addNotification(payload: Notification): NotificationAddAction {

View File

@ -5,7 +5,7 @@ import Peer from 'simple-peer'
import { Message } from '../actions/ChatActions' import { Message } from '../actions/ChatActions'
import { dismissNotification, Notification } 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, removeStream } 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 { Media } from './Media'
@ -25,6 +25,7 @@ export interface AppProps {
play: () => void play: () => void
sendMessage: (message: TextMessage) => void sendMessage: (message: TextMessage) => void
streams: Record<string, AddStreamPayload> streams: Record<string, AddStreamPayload>
removeStream: typeof removeStream
onSendFile: (file: File) => void onSendFile: (file: File) => void
toggleActive: (userId: string) => void toggleActive: (userId: string) => void
} }
@ -58,6 +59,9 @@ export default class App extends React.PureComponent<AppProps, AppState> {
const { init } = this.props const { init } = this.props
init() init()
} }
onHangup = () => {
this.props.removeStream(constants.ME)
}
render () { render () {
const { const {
active, active,
@ -81,12 +85,13 @@ export default class App extends React.PureComponent<AppProps, AppState> {
return ( return (
<div className="app"> <div className="app">
<Side align='end' left zIndex={2}> <Side align='flex-end' left zIndex={2}>
<Toolbar <Toolbar
chatVisible={this.state.chatVisible} chatVisible={this.state.chatVisible}
messagesCount={messagesCount} messagesCount={messagesCount}
onToggleChat={this.handleToggleChat} onToggleChat={this.handleToggleChat}
onSendFile={onSendFile} onSendFile={onSendFile}
onHangup={this.onHangup}
stream={streams[constants.ME]} stream={streams[constants.ME]}
/> />
</Side> </Side>

View File

@ -4,18 +4,27 @@ import { AudioConstraint, MediaDevice, setAudioConstraint, setVideoConstraint, V
import { MediaState } from '../reducers/media' import { MediaState } from '../reducers/media'
import { State } from '../store' import { State } from '../store'
import { Alerts, Alert } from './Alerts' import { Alerts, Alert } from './Alerts'
import { info, warning, error } from '../actions/NotifyActions'
import { ME } from '../constants'
export type MediaProps = MediaState & { export type MediaProps = MediaState & {
visible: boolean
enumerateDevices: typeof enumerateDevices enumerateDevices: typeof enumerateDevices
onSetVideoConstraint: typeof setVideoConstraint onSetVideoConstraint: typeof setVideoConstraint
onSetAudioConstraint: typeof setAudioConstraint onSetAudioConstraint: typeof setAudioConstraint
getMediaStream: typeof getMediaStream getMediaStream: typeof getMediaStream
play: typeof play play: typeof play
logInfo: typeof info
logWarning: typeof warning
logError: typeof error
} }
function mapStateToProps(state: State) { function mapStateToProps(state: State) {
const localStream = state.streams[ME]
const visible = !localStream
return { return {
...state.media, ...state.media,
visible,
} }
} }
@ -25,6 +34,9 @@ const mapDispatchToProps = {
onSetAudioConstraint: setAudioConstraint, onSetAudioConstraint: setAudioConstraint,
getMediaStream, getMediaStream,
play, play,
logInfo: info,
logWarning: warning,
logError: error,
} }
const c = connect(mapStateToProps, mapDispatchToProps) const c = connect(mapStateToProps, mapDispatchToProps)
@ -42,8 +54,7 @@ export const MediaForm = React.memo(function MediaForm(props: MediaProps) {
try { try {
await props.getMediaStream({ audio, video }) await props.getMediaStream({ audio, video })
} catch (err) { } catch (err) {
console.error(err.stack) props.logError('Error: {0}', err)
// TODO display a message
} }
} }

View File

@ -10,7 +10,7 @@ export type SideProps = (Left | Right | Top | Bottom) & {
className?: string className?: string
zIndex: number zIndex: number
children: React.ReactNode children: React.ReactNode
align?: 'baseline' | 'center' | 'end' align?: 'baseline' | 'center' | 'flex-end'
} }
export const Side = React.memo( export const Side = React.memo(

View File

@ -20,6 +20,7 @@ describe('components/Toolbar', () => {
return <Toolbar return <Toolbar
chatVisible={this.props.chatVisible} chatVisible={this.props.chatVisible}
onToggleChat={this.props.onToggleChat} onToggleChat={this.props.onToggleChat}
onHangup={this.props.onHangup}
onSendFile={this.props.onSendFile} onSendFile={this.props.onSendFile}
messagesCount={this.props.messagesCount} messagesCount={this.props.messagesCount}
stream={this.state.stream || this.props.stream} stream={this.state.stream || this.props.stream}
@ -32,16 +33,19 @@ describe('components/Toolbar', () => {
let url: string let url: string
let onToggleChat: jest.Mock<() => void> let onToggleChat: jest.Mock<() => void>
let onSendFile: jest.Mock<(file: File) => void> let onSendFile: jest.Mock<(file: File) => void>
let onHangup: jest.Mock<() => void>
async function render () { async function render () {
mediaStream = new MediaStream() mediaStream = new MediaStream()
onToggleChat = jest.fn() onToggleChat = jest.fn()
onSendFile = jest.fn() onSendFile = jest.fn()
onHangup = jest.fn()
const div = document.createElement('div') const div = document.createElement('div')
await new Promise<ToolbarWrapper>(resolve => { await new Promise<ToolbarWrapper>(resolve => {
ReactDOM.render( ReactDOM.render(
<ToolbarWrapper <ToolbarWrapper
ref={instance => resolve(instance!)} ref={instance => resolve(instance!)}
chatVisible chatVisible
onHangup={onHangup}
onToggleChat={onToggleChat} onToggleChat={onToggleChat}
onSendFile={onSendFile} onSendFile={onSendFile}
messagesCount={1} messagesCount={1}
@ -122,4 +126,14 @@ describe('components/Toolbar', () => {
}) })
}) })
describe('onHangup', () => {
it('calls onHangup callback', () => {
expect(onHangup.mock.calls.length).toBe(0)
const hangup = node.querySelector('.hangup')!
expect(hangup).toBeDefined()
TestUtils.Simulate.click(hangup)
expect(onHangup.mock.calls.length).toBe(1)
})
})
}) })

View File

@ -12,6 +12,7 @@ export interface ToolbarProps {
stream: AddStreamPayload stream: AddStreamPayload
onToggleChat: () => void onToggleChat: () => void
onSendFile: (file: File) => void onSendFile: (file: File) => void
onHangup: () => void
chatVisible: boolean chatVisible: boolean
} }
@ -38,17 +39,10 @@ function ToolbarButton(props: ToolbarButtonProps) {
const { blink, on } = props const { blink, on } = props
const icon = !on && props.offIcon ? props.offIcon : props.icon 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 ( return (
<a <a
className={classnames('button', props.className, { blink, on })} className={classnames('button', props.className, { blink, on })}
onClick={onClick} onClick={props.onClick}
href='#' href='#'
> >
<span className={classnames('icon', icon)}> <span className={classnames('icon', icon)}>
@ -181,12 +175,14 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
title='Toggle Fullscreen' title='Toggle Fullscreen'
/> />
{this.props.stream && this.props.stream.stream && (
<ToolbarButton <ToolbarButton
onClick={this.handleHangoutClick} onClick={this.props.onHangup}
className='hangup' className='hangup'
icon='icon-call_end' icon='icon-call_end'
title="Hang Up" title="Hang Up"
/> />
)}
</div> </div>
) )

View File

@ -20,7 +20,6 @@ export const MEDIA_STREAM = 'MEDIA_STREAM'
export const MEDIA_VIDEO_CONSTRAINT_SET = 'MEDIA_VIDEO_CONSTRAINT_SET' export const MEDIA_VIDEO_CONSTRAINT_SET = 'MEDIA_VIDEO_CONSTRAINT_SET'
export const MEDIA_AUDIO_CONSTRAINT_SET = 'MEDIA_AUDIO_CONSTRAINT_SET' export const MEDIA_AUDIO_CONSTRAINT_SET = 'MEDIA_AUDIO_CONSTRAINT_SET'
export const MEDIA_PLAY = 'MEDIA_PLAY' export const MEDIA_PLAY = 'MEDIA_PLAY'
export const MEDIA_VISIBLE_SET = 'MEDIA_VISIBLE_SET'
export const PEER_ADD = 'PEER_ADD' export const PEER_ADD = 'PEER_ADD'
export const PEER_REMOVE = 'PEER_REMOVE' export const PEER_REMOVE = 'PEER_REMOVE'

View File

@ -3,7 +3,7 @@ import { init } from '../actions/CallActions'
import { play } from '../actions/MediaActions' import { play } from '../actions/MediaActions'
import { dismissNotification } from '../actions/NotifyActions' import { dismissNotification } from '../actions/NotifyActions'
import { sendFile, sendMessage } from '../actions/PeerActions' import { sendFile, sendMessage } from '../actions/PeerActions'
import { toggleActive } from '../actions/StreamActions' import { toggleActive, removeStream } from '../actions/StreamActions'
import App from '../components/App' import App from '../components/App'
import { State } from '../store' import { State } from '../store'
@ -22,6 +22,7 @@ const mapDispatchToProps = {
toggleActive, toggleActive,
sendMessage, sendMessage,
dismissNotification, dismissNotification,
removeStream,
init, init,
onSendFile: sendFile, onSendFile: sendFile,
play, play,

View File

@ -1,5 +1,5 @@
import { MediaDevice, AudioConstraint, VideoConstraint, MediaAction, MediaEnumerateAction, MediaStreamAction, MediaPlayAction } from '../actions/MediaActions' import { MediaDevice, AudioConstraint, VideoConstraint, MediaAction, MediaEnumerateAction, MediaStreamAction, MediaPlayAction } from '../actions/MediaActions'
import { MEDIA_ENUMERATE, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_VISIBLE_SET, MEDIA_STREAM, MEDIA_PLAY } from '../constants' import { MEDIA_ENUMERATE, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_STREAM, MEDIA_PLAY } from '../constants'
export interface MediaState { export interface MediaState {
devices: MediaDevice[] devices: MediaDevice[]
@ -8,7 +8,6 @@ export interface MediaState {
loading: boolean loading: boolean
error: string error: string
autoplayError: boolean autoplayError: boolean
visible: boolean
} }
const defaultState: MediaState = { const defaultState: MediaState = {
@ -18,7 +17,6 @@ const defaultState: MediaState = {
loading: false, loading: false,
error: '', error: '',
autoplayError: false, autoplayError: false,
visible: true,
} }
export function handleEnumerate( export function handleEnumerate(
@ -54,13 +52,11 @@ export function handleMediaStream(
case 'resolved': case 'resolved':
return { return {
...state, ...state,
visible: false,
} }
case 'rejected': case 'rejected':
return { return {
...state, ...state,
error: action.payload.message, error: action.payload.message,
visible: true,
} }
default: default:
return state return state
@ -105,11 +101,6 @@ export default function media(
...state, ...state,
video: action.payload, video: action.payload,
} }
case MEDIA_VISIBLE_SET:
return {
...state,
visible: action.payload.visible,
}
case MEDIA_STREAM: case MEDIA_STREAM:
return handleMediaStream(state, action) return handleMediaStream(state, action)
case MEDIA_PLAY: case MEDIA_PLAY:

View File

@ -1,5 +1,5 @@
.media-container form.media { .media-container form.media {
margin: 1rem auto 0; margin: 4rem auto 0;
max-width: 450px; max-width: 450px;
text-align: center; text-align: center;

View File

@ -42,6 +42,12 @@
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%);
pointer-events: none; pointer-events: none;
position: absolute;
left: 100%;
top: 0;
bottom: 0;
white-space: nowrap;
line-height: 48px;
} }
.button { .button {
@ -51,8 +57,9 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
position: relative;
&:hover, &:focus { &:hover {
.icon { .icon {
box-shadow: 4px 4px 48px #666; box-shadow: 4px 4px 48px #666;
cursor: pointer; cursor: pointer;