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 { 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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarButton
|
{this.props.stream && this.props.stream.stream && (
|
||||||
onClick={this.handleHangoutClick}
|
<ToolbarButton
|
||||||
className='hangup'
|
onClick={this.props.onHangup}
|
||||||
icon='icon-call_end'
|
className='hangup'
|
||||||
title="Hang Up"
|
icon='icon-call_end'
|
||||||
/>
|
title="Hang Up"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user