Fix toolbar icons. Hangup removes video stream
This commit is contained in:
parent
a097e26a20
commit
c89886bbfa
@ -1,5 +1,8 @@
|
||||
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 {
|
||||
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 () => {
|
||||
const promises = Array
|
||||
.from(document.querySelectorAll('video'))
|
||||
@ -114,7 +104,10 @@ export const play = makeAction('MEDIA_PLAY', async () => {
|
||||
|
||||
export const getMediaStream = makeAction(
|
||||
MEDIA_STREAM,
|
||||
async (constraints: GetMediaConstraints) => getUserMedia(constraints),
|
||||
async (constraints: GetMediaConstraints) => {
|
||||
debug('getMediaStream', constraints)
|
||||
return getUserMedia(constraints)
|
||||
},
|
||||
)
|
||||
|
||||
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
|
||||
@ -126,5 +119,4 @@ export type MediaAction =
|
||||
MediaAudioConstraintAction |
|
||||
MediaEnumerateAction |
|
||||
MediaStreamAction |
|
||||
MediaVisibleAction |
|
||||
MediaPlayAction
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import uniqueId from 'lodash/uniqueId'
|
||||
import { Dispatch } from 'redux'
|
||||
import * as constants from '../constants'
|
||||
import { ThunkResult } from '../store'
|
||||
|
||||
function format (string: string, args: string[]) {
|
||||
string = args
|
||||
@ -11,7 +9,7 @@ function format (string: string, args: string[]) {
|
||||
|
||||
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 message = format(string, Array.prototype.slice.call(args, 1))
|
||||
const id = uniqueId('notification')
|
||||
@ -20,16 +18,16 @@ function notify(dispatch: Dispatch, type: NotifyType, args: string[]) {
|
||||
return addNotification(payload)
|
||||
}
|
||||
|
||||
export const info = (...args: any[]): ThunkResult<NotificationAddAction> => {
|
||||
return dispatch => dispatch(notify(dispatch, 'info', args))
|
||||
export const info = (...args: any[]): NotificationAddAction => {
|
||||
return notify('info', args)
|
||||
}
|
||||
|
||||
export const warning = (...args: any[]): ThunkResult<NotificationAddAction> => {
|
||||
return dispatch => dispatch(notify(dispatch, 'warning', args))
|
||||
export const warning = (...args: any[]): NotificationAddAction => {
|
||||
return notify('warning', args)
|
||||
}
|
||||
|
||||
export const error = (...args: any[]): ThunkResult<NotificationAddAction> => {
|
||||
return dispatch => dispatch(notify(dispatch, 'error', args))
|
||||
export const error = (...args: any[]): NotificationAddAction => {
|
||||
return notify('error', args)
|
||||
}
|
||||
|
||||
function addNotification(payload: Notification): NotificationAddAction {
|
||||
|
||||
@ -5,7 +5,7 @@ import Peer from 'simple-peer'
|
||||
import { Message } from '../actions/ChatActions'
|
||||
import { dismissNotification, Notification } from '../actions/NotifyActions'
|
||||
import { TextMessage } from '../actions/PeerActions'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
import { AddStreamPayload, removeStream } from '../actions/StreamActions'
|
||||
import * as constants from '../constants'
|
||||
import Chat from './Chat'
|
||||
import { Media } from './Media'
|
||||
@ -25,6 +25,7 @@ export interface AppProps {
|
||||
play: () => void
|
||||
sendMessage: (message: TextMessage) => void
|
||||
streams: Record<string, AddStreamPayload>
|
||||
removeStream: typeof removeStream
|
||||
onSendFile: (file: File) => void
|
||||
toggleActive: (userId: string) => void
|
||||
}
|
||||
@ -58,6 +59,9 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
const { init } = this.props
|
||||
init()
|
||||
}
|
||||
onHangup = () => {
|
||||
this.props.removeStream(constants.ME)
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
active,
|
||||
@ -81,12 +85,13 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Side align='end' left zIndex={2}>
|
||||
<Side align='flex-end' left zIndex={2}>
|
||||
<Toolbar
|
||||
chatVisible={this.state.chatVisible}
|
||||
messagesCount={messagesCount}
|
||||
onToggleChat={this.handleToggleChat}
|
||||
onSendFile={onSendFile}
|
||||
onHangup={this.onHangup}
|
||||
stream={streams[constants.ME]}
|
||||
/>
|
||||
</Side>
|
||||
|
||||
@ -4,18 +4,27 @@ import { AudioConstraint, MediaDevice, setAudioConstraint, setVideoConstraint, V
|
||||
import { MediaState } from '../reducers/media'
|
||||
import { State } from '../store'
|
||||
import { Alerts, Alert } from './Alerts'
|
||||
import { info, warning, error } from '../actions/NotifyActions'
|
||||
import { ME } from '../constants'
|
||||
|
||||
export type MediaProps = MediaState & {
|
||||
visible: boolean
|
||||
enumerateDevices: typeof enumerateDevices
|
||||
onSetVideoConstraint: typeof setVideoConstraint
|
||||
onSetAudioConstraint: typeof setAudioConstraint
|
||||
getMediaStream: typeof getMediaStream
|
||||
play: typeof play
|
||||
logInfo: typeof info
|
||||
logWarning: typeof warning
|
||||
logError: typeof error
|
||||
}
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
const localStream = state.streams[ME]
|
||||
const visible = !localStream
|
||||
return {
|
||||
...state.media,
|
||||
visible,
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +34,9 @@ const mapDispatchToProps = {
|
||||
onSetAudioConstraint: setAudioConstraint,
|
||||
getMediaStream,
|
||||
play,
|
||||
logInfo: info,
|
||||
logWarning: warning,
|
||||
logError: error,
|
||||
}
|
||||
|
||||
const c = connect(mapStateToProps, mapDispatchToProps)
|
||||
@ -42,8 +54,7 @@ export const MediaForm = React.memo(function MediaForm(props: MediaProps) {
|
||||
try {
|
||||
await props.getMediaStream({ audio, video })
|
||||
} catch (err) {
|
||||
console.error(err.stack)
|
||||
// TODO display a message
|
||||
props.logError('Error: {0}', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ export type SideProps = (Left | Right | Top | Bottom) & {
|
||||
className?: string
|
||||
zIndex: number
|
||||
children: React.ReactNode
|
||||
align?: 'baseline' | 'center' | 'end'
|
||||
align?: 'baseline' | 'center' | 'flex-end'
|
||||
}
|
||||
|
||||
export const Side = React.memo(
|
||||
|
||||
@ -20,6 +20,7 @@ describe('components/Toolbar', () => {
|
||||
return <Toolbar
|
||||
chatVisible={this.props.chatVisible}
|
||||
onToggleChat={this.props.onToggleChat}
|
||||
onHangup={this.props.onHangup}
|
||||
onSendFile={this.props.onSendFile}
|
||||
messagesCount={this.props.messagesCount}
|
||||
stream={this.state.stream || this.props.stream}
|
||||
@ -32,16 +33,19 @@ describe('components/Toolbar', () => {
|
||||
let url: string
|
||||
let onToggleChat: jest.Mock<() => void>
|
||||
let onSendFile: jest.Mock<(file: File) => void>
|
||||
let onHangup: jest.Mock<() => void>
|
||||
async function render () {
|
||||
mediaStream = new MediaStream()
|
||||
onToggleChat = jest.fn()
|
||||
onSendFile = jest.fn()
|
||||
onHangup = jest.fn()
|
||||
const div = document.createElement('div')
|
||||
await new Promise<ToolbarWrapper>(resolve => {
|
||||
ReactDOM.render(
|
||||
<ToolbarWrapper
|
||||
ref={instance => resolve(instance!)}
|
||||
chatVisible
|
||||
onHangup={onHangup}
|
||||
onToggleChat={onToggleChat}
|
||||
onSendFile={onSendFile}
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -12,6 +12,7 @@ export interface ToolbarProps {
|
||||
stream: AddStreamPayload
|
||||
onToggleChat: () => void
|
||||
onSendFile: (file: File) => void
|
||||
onHangup: () => void
|
||||
chatVisible: boolean
|
||||
}
|
||||
|
||||
@ -38,17 +39,10 @@ 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}
|
||||
onClick={props.onClick}
|
||||
href='#'
|
||||
>
|
||||
<span className={classnames('icon', icon)}>
|
||||
@ -181,12 +175,14 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
||||
title='Toggle Fullscreen'
|
||||
/>
|
||||
|
||||
<ToolbarButton
|
||||
onClick={this.handleHangoutClick}
|
||||
className='hangup'
|
||||
icon='icon-call_end'
|
||||
title="Hang Up"
|
||||
/>
|
||||
{this.props.stream && this.props.stream.stream && (
|
||||
<ToolbarButton
|
||||
onClick={this.props.onHangup}
|
||||
className='hangup'
|
||||
icon='icon-call_end'
|
||||
title="Hang Up"
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -20,7 +20,6 @@ export const MEDIA_STREAM = 'MEDIA_STREAM'
|
||||
export const MEDIA_VIDEO_CONSTRAINT_SET = 'MEDIA_VIDEO_CONSTRAINT_SET'
|
||||
export const MEDIA_AUDIO_CONSTRAINT_SET = 'MEDIA_AUDIO_CONSTRAINT_SET'
|
||||
export const MEDIA_PLAY = 'MEDIA_PLAY'
|
||||
export const MEDIA_VISIBLE_SET = 'MEDIA_VISIBLE_SET'
|
||||
|
||||
export const PEER_ADD = 'PEER_ADD'
|
||||
export const PEER_REMOVE = 'PEER_REMOVE'
|
||||
|
||||
@ -3,7 +3,7 @@ import { init } from '../actions/CallActions'
|
||||
import { play } from '../actions/MediaActions'
|
||||
import { dismissNotification } from '../actions/NotifyActions'
|
||||
import { sendFile, sendMessage } from '../actions/PeerActions'
|
||||
import { toggleActive } from '../actions/StreamActions'
|
||||
import { toggleActive, removeStream } from '../actions/StreamActions'
|
||||
import App from '../components/App'
|
||||
import { State } from '../store'
|
||||
|
||||
@ -22,6 +22,7 @@ const mapDispatchToProps = {
|
||||
toggleActive,
|
||||
sendMessage,
|
||||
dismissNotification,
|
||||
removeStream,
|
||||
init,
|
||||
onSendFile: sendFile,
|
||||
play,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
devices: MediaDevice[]
|
||||
@ -8,7 +8,6 @@ export interface MediaState {
|
||||
loading: boolean
|
||||
error: string
|
||||
autoplayError: boolean
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
const defaultState: MediaState = {
|
||||
@ -18,7 +17,6 @@ const defaultState: MediaState = {
|
||||
loading: false,
|
||||
error: '',
|
||||
autoplayError: false,
|
||||
visible: true,
|
||||
}
|
||||
|
||||
export function handleEnumerate(
|
||||
@ -54,13 +52,11 @@ export function handleMediaStream(
|
||||
case 'resolved':
|
||||
return {
|
||||
...state,
|
||||
visible: false,
|
||||
}
|
||||
case 'rejected':
|
||||
return {
|
||||
...state,
|
||||
error: action.payload.message,
|
||||
visible: true,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
@ -105,11 +101,6 @@ export default function media(
|
||||
...state,
|
||||
video: action.payload,
|
||||
}
|
||||
case MEDIA_VISIBLE_SET:
|
||||
return {
|
||||
...state,
|
||||
visible: action.payload.visible,
|
||||
}
|
||||
case MEDIA_STREAM:
|
||||
return handleMediaStream(state, action)
|
||||
case MEDIA_PLAY:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.media-container form.media {
|
||||
margin: 1rem auto 0;
|
||||
margin: 4rem auto 0;
|
||||
max-width: 450px;
|
||||
text-align: center;
|
||||
|
||||
|
||||
@ -42,6 +42,12 @@
|
||||
transition: opacity 200ms ease-in 25ms, transform 100ms ease-in;
|
||||
transform: translateX(-100%);
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
white-space: nowrap;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
.button {
|
||||
@ -51,8 +57,9 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover, &:focus {
|
||||
&:hover {
|
||||
.icon {
|
||||
box-shadow: 4px 4px 48px #666;
|
||||
cursor: pointer;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user