Update Calls (#5875)

* Fix adding tracks

* Use server websocket handler to do signaling

* Start using react-native-webrtc fork to fix track issues

* Update deps lock files

* Use ICE servers list from config if present

* Properly replace voice track

* Remove translations

* Use DeviceEventEmitter to handle voice events

* Fix bad checksum

* Restore non-english translations to avoid conflicts

* Address review

* Remove flaky integrity

* Improve state handling

* Update snapshots and tests

* Update call connecting logic

* Fix permissions check
This commit is contained in:
Claudio Costa 2022-01-27 08:19:26 +01:00 committed by GitHub
parent c06a24c1ce
commit 2267f0a408
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 693 additions and 638 deletions

View file

@ -2795,14 +2795,12 @@ SOFTWARE.
---
## react-native-webrtc2
## react-native-webrtc
This product contains a modified version of 'react-native-webrtc2' by juncocoa.
React Native 的 WebRTC 模块
This product contains a modified version of 'react-native-webrtc'.
* HOMEPAGE:
* https://github.com/juncocoa/react-native-webrtc2
* https://github.com/react-native-webrtc/react-native-webrtc
* LICENSE: MIT

View file

@ -446,7 +446,8 @@ function handleEvent(msg: WebSocketMessage) {
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
return dispatch(handleCallChannelDisabled(msg));
case WebsocketEvents.CALLS_USER_CONNECTED:
return dispatch(handleCallUserConnected(msg));
handleCallUserConnected(dispatch, getState, msg);
break;
case WebsocketEvents.CALLS_USER_DISCONNECTED:
return dispatch(handleCallUserDisconnected(msg));
case WebsocketEvents.CALLS_USER_MUTED:
@ -454,9 +455,11 @@ function handleEvent(msg: WebSocketMessage) {
case WebsocketEvents.CALLS_USER_UNMUTED:
return dispatch(handleCallUserUnmuted(msg));
case WebsocketEvents.CALLS_USER_VOICE_ON:
return dispatch(handleCallUserVoiceOn(msg));
handleCallUserVoiceOn(msg);
break;
case WebsocketEvents.CALLS_USER_VOICE_OFF:
return dispatch(handleCallUserVoiceOff(msg));
handleCallUserVoiceOff(msg);
break;
case WebsocketEvents.CALLS_CALL_START:
return dispatch(handleCallStarted(msg));
case WebsocketEvents.CALLS_SCREEN_ON:

View file

@ -17,6 +17,13 @@ const ClientCalls = (superclass: any) => class extends superclass {
);
};
getCallsConfig = async () => {
return this.doFetch(
`${this.getCallsRoute()}/config`,
{method: 'get'},
);
};
enableChannelCalls = async (channelId: string) => {
return this.doFetch(
`${this.getCallsRoute()}/${channelId}`,

View file

@ -40,7 +40,6 @@ exports[`EnableDisableCalls should match snapshot if calls are disabled 1`] = `
rightArrow={false}
shouldRender={true}
testID="test-id"
textId="mobile.channel_info.enable_calls"
theme={
Object {
"awayIndicator": "#ffbc1f",
@ -115,7 +114,6 @@ exports[`EnableDisableCalls should match snapshot if calls are enabled 1`] = `
rightArrow={false}
shouldRender={true}
testID="test-id"
textId="mobile.channel_info.disable_calls"
theme={
Object {
"awayIndicator": "#ffbc1f",

View file

@ -8,7 +8,7 @@ import CompassIcon from '@components/compass_icon';
import ProfilePicture from '@components/profile_picture';
type Props = {
userId: string;
userId?: string;
volume: number;
muted?: boolean;
size?: 'm' | 'l';
@ -63,11 +63,17 @@ const CallAvatar = (props: Props) => {
<View style={style.pictureHalo}>
<View style={style.pictureHalo2}>
<View style={style.picture}>
<ProfilePicture
userId={props.userId}
size={props.size === 'm' || !props.size ? 40 : 72}
showStatus={false}
/>
{props.userId ?
<ProfilePicture
userId={props.userId}
size={props.size === 'm' || !props.size ? 40 : 72}
showStatus={false}
/> :
<CompassIcon
name='account-outline'
size={props.size === 'm' || !props.size ? 40 : 72}
/>
}
{props.muted !== undefined &&
<CompassIcon
name={props.muted ? 'microphone-off' : 'microphone'}

View file

@ -34,21 +34,16 @@ exports[`CallMessage should match snapshot 1`] = `
}
}
>
<InjectIntl(FormattedText)
defaultMessage="{user} started a call"
id="call_message.started_a_call"
<Text
style={
Object {
"color": "#3f4350",
"fontWeight": "bold",
}
}
values={
Object {
"user": "User 1",
}
}
/>
>
User 1 started a call
</Text>
<FormattedRelativeTime
style={
Object {
@ -82,15 +77,15 @@ exports[`CallMessage should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Join Call"
id="call_message.join_call"
<Text
style={
Object {
"color": "white",
}
}
/>
>
Join call
</Text>
</TouchableOpacity>
</View>
`;
@ -129,16 +124,16 @@ exports[`CallMessage should match snapshot for ended call 1`] = `
}
}
>
<InjectIntl(FormattedText)
defaultMessage="Call ended"
id="call_message.call_ended"
<Text
style={
Object {
"color": "#3f4350",
"fontWeight": "bold",
}
}
/>
>
Call ended
</Text>
<View
style={
Object {
@ -148,23 +143,19 @@ exports[`CallMessage should match snapshot for ended call 1`] = `
}
}
>
<InjectIntl(FormattedText)
defaultMessage="Ended at {time}"
id="call_message.call_ended_at"
<Text
style={
Object {
"color": "#3f4350",
}
}
values={
Object {
"time": <FormattedTime
isMilitaryTime={false}
timezone="utc"
value={200}
/>,
}
}
>
Ended at
</Text>
<FormattedTime
isMilitaryTime={false}
timezone="utc"
value={200}
/>
<Text
style={
@ -177,20 +168,15 @@ exports[`CallMessage should match snapshot for ended call 1`] = `
>
</Text>
<InjectIntl(FormattedText)
defaultMessage="Lasted {duration}"
id="call_message.call_lasted"
<Text
style={
Object {
"color": "#3f4350",
}
}
values={
Object {
"duration": "a few seconds",
}
}
/>
>
Lasted a few seconds
</Text>
</View>
</View>
</View>
@ -230,21 +216,16 @@ exports[`CallMessage should match snapshot for the call already in the current c
}
}
>
<InjectIntl(FormattedText)
defaultMessage="{user} started a call"
id="call_message.started_a_call"
<Text
style={
Object {
"color": "#3f4350",
"fontWeight": "bold",
}
}
values={
Object {
"user": "User 1",
}
}
/>
>
User 1 started a call
</Text>
<FormattedRelativeTime
style={
Object {
@ -278,15 +259,15 @@ exports[`CallMessage should match snapshot for the call already in the current c
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Current Call"
id="call_message.current_call"
<Text
style={
Object {
"color": "white",
}
}
/>
>
Current call
</Text>
</TouchableOpacity>
</View>
`;

View file

@ -8,7 +8,6 @@ import {View, TouchableOpacity, Text} from 'react-native';
import CompassIcon from '@components/compass_icon';
import FormattedRelativeTime from '@components/formatted_relative_time';
import FormattedText from '@components/formatted_text';
import FormattedTime from '@components/formatted_time';
import {displayUsername} from '@mm-redux/utils/user_utils';
import leaveAndJoinWithAlert from '@mmproducts/calls/components/leave_and_join_alert';
@ -117,37 +116,26 @@ const CallMessage = ({post, user, teammateNameDisplay, confirmToJoin, alreadyInT
style={style.phoneHangupIcon}
/>
<View style={style.messageText}>
<FormattedText
id='call_message.call_ended'
defaultMessage='Call ended'
<Text
style={style.startedText}
/>
>{'Call ended'}</Text>
<View
style={style.endCallInfo}
>
<FormattedText
id='call_message.call_ended_at'
defaultMessage='Ended at {time}'
<Text
style={style.timeText}
values={{
time: (
<FormattedTime
value={post.props.end_at}
isMilitaryTime={isMilitaryTime}
timezone={userTimezone}
/>
),
}}
/>
>{'Ended at '}</Text>
{
<FormattedTime
value={post.props.end_at}
isMilitaryTime={isMilitaryTime}
timezone={userTimezone}
/>
}
<Text style={style.separator}>{'•'}</Text>
<FormattedText
<Text
style={style.timeText}
id='call_message.call_lasted'
defaultMessage='Lasted {duration}'
values={{
duration: moment.duration(post.props.end_at - post.props.start_at).humanize(false),
}}
/>
>{`Lasted ${moment.duration(post.props.end_at - post.props.start_at).humanize(false)}`}</Text>
</View>
</View>
</View>
@ -162,12 +150,9 @@ const CallMessage = ({post, user, teammateNameDisplay, confirmToJoin, alreadyInT
style={style.joinCallIcon}
/>
<View style={style.messageText}>
<FormattedText
id='call_message.started_a_call'
defaultMessage='{user} started a call'
values={{user: displayUsername(user, teammateNameDisplay)}}
<Text
style={style.startedText}
/>
>{`${displayUsername(user, teammateNameDisplay)} started a call`}</Text>
<FormattedRelativeTime
value={post.props.start_at}
updateIntervalInSeconds={1}
@ -185,17 +170,13 @@ const CallMessage = ({post, user, teammateNameDisplay, confirmToJoin, alreadyInT
style={style.joinCallButtonIcon}
/>
{alreadyInTheCall &&
<FormattedText
id='call_message.current_call'
defaultMessage='Current Call'
<Text
style={style.joinCallButtonText}
/>}
>{'Current call'}</Text>}
{!alreadyInTheCall &&
<FormattedText
id='call_message.join_call'
defaultMessage='Join Call'
<Text
style={style.joinCallButtonText}
/>}
>{'Join call'}</Text>}
</TouchableOpacity>
</View>
);

View file

@ -22,8 +22,8 @@ exports[`CurrentCall should match snapshot muted 1`] = `
}
>
<CallAvatar
userId="user-1-id"
volume={0.5}
userId=""
volume={0}
/>
<View
style={
@ -41,15 +41,7 @@ exports[`CurrentCall should match snapshot muted 1`] = `
}
}
>
<InjectIntl(FormattedText)
defaultMessage="{username} is speaking"
id="current_call.user-is-speaking"
values={
Object {
"username": "User 1",
}
}
/>
No one is talking
</Text>
<Text
style={
@ -59,15 +51,7 @@ exports[`CurrentCall should match snapshot muted 1`] = `
}
}
>
<InjectIntl(FormattedText)
defaultMessage="~{channelName}"
id="current_call.channel-name"
values={
Object {
"channelName": "Channel Name",
}
}
/>
~Channel Name
</Text>
</View>
<Pressable
@ -149,8 +133,8 @@ exports[`CurrentCall should match snapshot unmuted 1`] = `
}
>
<CallAvatar
userId="user-1-id"
volume={0.5}
userId=""
volume={0}
/>
<View
style={
@ -168,15 +152,7 @@ exports[`CurrentCall should match snapshot unmuted 1`] = `
}
}
>
<InjectIntl(FormattedText)
defaultMessage="{username} is speaking"
id="current_call.user-is-speaking"
values={
Object {
"username": "User 1",
}
}
/>
No one is talking
</Text>
<Text
style={
@ -186,15 +162,7 @@ exports[`CurrentCall should match snapshot unmuted 1`] = `
}
}
>
<InjectIntl(FormattedText)
defaultMessage="~{channelName}"
id="current_call.channel-name"
values={
Object {
"channelName": "Channel Name",
}
}
/>
~Channel Name
</Text>
</View>
<Pressable

View file

@ -1,13 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect} from 'react';
import {View, Text, TouchableOpacity, Pressable, Platform} from 'react-native';
import React, {useCallback, useEffect, useState} from 'react';
import {View, Text, TouchableOpacity, Pressable, Platform, DeviceEventEmitter} from 'react-native';
import {Options} from 'react-native-navigation';
import {goToScreen} from '@actions/navigation';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {WebsocketEvents} from '@constants';
import ViewTypes, {CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
import {GenericAction} from '@mm-redux/types/actions';
import EventEmitter from '@mm-redux/utils/event_emitter';
@ -18,7 +18,7 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
import type {Channel} from '@mm-redux/types/channels';
import type {Theme} from '@mm-redux/types/theme';
import type {UserProfile} from '@mm-redux/types/users';
import type {Call, CallParticipant} from '@mmproducts/calls/store/types/calls';
import type {Call, CallParticipant, VoiceEventData} from '@mmproducts/calls/store/types/calls';
type Props = {
actions: {
@ -27,8 +27,6 @@ type Props = {
};
theme: Theme;
channel: Channel;
speaker: CallParticipant;
speakerUser: UserProfile;
call: Call;
currentParticipant: CallParticipant;
teammateNameDisplay: string;
@ -88,10 +86,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
});
const CurrentCall = (props: Props) => {
const [speaker, setSpeaker] = useState<UserProfile|null>(null);
const handleVoiceOn = (data: VoiceEventData) => {
if (data.channelId === props.call?.channelId) {
setSpeaker(props.call.participants[data.userId].profile);
}
};
const handleVoiceOff = (data: VoiceEventData) => {
if (data.channelId === props.call?.channelId && ((speaker?.id === data.userId) || !speaker)) {
setSpeaker(null);
}
};
useEffect(() => {
const onVoiceOn = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_ON, handleVoiceOn);
const onVoiceOff = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_OFF, handleVoiceOff);
EventEmitter.emit(ViewTypes.CURRENT_CALL_BAR_VISIBLE, Boolean(props.call));
return () => {
EventEmitter.emit(ViewTypes.CURRENT_CALL_BAR_VISIBLE, Boolean(false));
onVoiceOn.remove();
onVoiceOff.remove();
};
}, [props.call]);
@ -129,23 +143,16 @@ const CurrentCall = (props: Props) => {
<View style={style.wrapper}>
<View style={style.container}>
<CallAvatar
userId={props.speaker?.id}
volume={props.speaker?.isTalking ? 0.5 : 0}
userId={speaker?.id ?? ''}
volume={speaker?.id ? 0.5 : 0}
/>
<View style={style.userInfo}>
<Text style={style.speakingUser}>
<FormattedText
id='current_call.user-is-speaking'
defaultMessage='{username} is speaking'
values={{username: displayUsername(props.speakerUser, props.teammateNameDisplay)}}
/>
{speaker && `${displayUsername(speaker, props.teammateNameDisplay)} is talking`}
{!speaker && 'No one is talking'}
</Text>
<Text style={style.currentChannel}>
<FormattedText
id='current_call.channel-name'
defaultMessage='~{channelName}'
values={{channelName: props.channel.display_name}}
/>
{`~${props.channel.display_name}`}
</Text>
</View>
<Pressable

View file

@ -16,14 +16,11 @@ import type {GlobalState} from '@mm-redux/types/store';
function mapStateToProps(state: GlobalState) {
const currentCall = getCurrentCall(state);
const currentUserId = getCurrentUserId(state);
const speakerId = currentCall && currentCall.speakers && currentCall.speakers[0];
const speaker = currentCall && ((speakerId && currentCall.participants[speakerId]) || Object.values(currentCall.participants)[0]);
const currentParticipant = currentCall?.participants[currentUserId];
return {
theme: getTheme(state),
call: currentCall,
speaker,
speakerUser: speaker ? state.entities.users.profiles[speaker.id] : null,
channel: getChannel(state, currentCall?.channelId || ''),
currentParticipant,
teammateNameDisplay: getTeammateNameDisplaySetting(state),

View file

@ -6,7 +6,6 @@ import React, {useCallback} from 'react';
import {Theme} from '@mm-redux/types/theme';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
type Props = {
@ -34,7 +33,6 @@ const EnableDisableCalls = (props: Props) => {
action={handleEnableDisableCalls}
defaultMessage={enabled ? 'Disable Calls' : 'Enable Calls'}
icon='phone-outline'
textId={enabled ? t('mobile.channel_info.disable_calls') : t('mobile.channel_info.enable_calls')}
theme={theme}
rightArrow={false}
/>

View file

@ -62,9 +62,7 @@ exports[`JoinCall should match snapshot 1`] = `
<Connect(Avatars)
breakAt={1}
listTitle={
<InjectIntl(FormattedText)
defaultMessage="CALL PARTICIPANTS"
id="calls.join_call.participants_list_header"
<Text
style={
Object {
"color": "rgba(63,67,80,0.56)",
@ -75,7 +73,9 @@ exports[`JoinCall should match snapshot 1`] = `
"top": 16,
}
}
/>
>
Call participants
</Text>
}
userIds={
Array [

View file

@ -8,7 +8,6 @@ import {View, Text, Pressable} from 'react-native';
import Avatars from '@components/avatars';
import CompassIcon from '@components/compass_icon';
import FormattedRelativeTime from '@components/formatted_relative_time';
import FormattedText from '@components/formatted_text';
import ViewTypes, {JOIN_CALL_BAR_HEIGHT} from '@constants/view';
import EventEmitter from '@mm-redux/utils/event_emitter';
import leaveAndJoinWithAlert from '@mmproducts/calls/components/leave_and_join_alert';
@ -118,11 +117,7 @@ const JoinCall = (props: Props) => {
userIds={userIds}
breakAt={1}
listTitle={
<FormattedText
id='calls.join_call.participants_list_header'
defaultMessage={'CALL PARTICIPANTS'}
style={style.headerText}
/>
<Text style={style.headerText}>{'Call participants'}</Text>
}
/>
</View>

View file

@ -7,17 +7,14 @@ import {Alert} from 'react-native';
export default function leaveAndJoinWithAlert(intl: typeof IntlShape, channelId: string, callChannelName: string, currentChannelName: string, confirmToJoin: boolean, joinCall: (channelId: string) => void) {
if (confirmToJoin) {
Alert.alert(
intl.formatMessage({id: 'calls.confirm-to-join-title', defaultMessage: 'Are you sure you want to switch to a different call?'}),
intl.formatMessage({
id: 'calls.confirm-to-join-description',
defaultMessage: 'You are already on a channel call in ~{callChannelName}. Do you want to leave your current call and join the call in ~{currentChannelName}?',
}, {callChannelName, currentChannelName}),
'Are you sure you want to switch to a different call?',
`You are already on a channel call in ~${callChannelName}. Do you want to leave your current call and join the call in ~${currentChannelName}?`,
[
{
text: intl.formatMessage({id: 'calls.confirm-to-join-cancel', defaultMessage: 'Cancel'}),
text: 'Cancel',
},
{
text: intl.formatMessage({id: 'calls.confirm-to-join-leave-and-join', defaultMessage: 'Leave & Join'}),
text: 'Leave & Join',
onPress: () => joinCall(channelId),
style: 'cancel',
},

View file

@ -40,7 +40,6 @@ exports[`StartCall should match snapshot 1`] = `
rightArrow={false}
shouldRender={true}
testID="test-id"
textId="mobile.channel_info.start_call"
theme={
Object {
"awayIndicator": "#ffbc1f",
@ -115,7 +114,6 @@ exports[`StartCall should match snapshot when there is already an ongoing call i
rightArrow={false}
shouldRender={true}
testID="test-id"
textId="mobile.channel_info.join_ongoing_call"
theme={
Object {
"awayIndicator": "#ffbc1f",

View file

@ -8,7 +8,6 @@ import {Theme} from '@mm-redux/types/theme';
import leaveAndJoinWithAlert from '@mmproducts/calls/components/leave_and_join_alert';
import ChannelInfoRow from '@screens/channel_info/channel_info_row';
import Separator from '@screens/channel_info/separator';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
type Props = {
@ -50,7 +49,6 @@ const StartCall = (props: Props) => {
action={handleStartCall}
defaultMessage={ongoingCall ? 'Join Ongoing Call' : 'Start Call'}
icon='phone-in-talk'
textId={ongoingCall ? t('mobile.channel_info.join_ongoing_call') : t('mobile.channel_info.start_call')}
theme={theme}
rightArrow={false}
/>

View file

@ -1,50 +1,51 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import {deflate} from 'pako/lib/deflate.js';
import InCallManager from 'react-native-incall-manager';
import {
MediaStream,
MediaStreamTrack,
mediaDevices,
} from 'react-native-webrtc2';
} from 'react-native-webrtc';
import {Client4} from '@client/rest';
import Peer from './simple-peer';
import WebSocketClient from './websocket';
export let client: any = null;
const websocketConnectTimeout = 3000;
function getWSConnectionURL(channelID: string): string {
let url = Client4.getAbsoluteUrl(`/plugins/com.mattermost.calls/${channelID}/ws`);
url = url.replace(/^https:/, 'wss:');
url = url.replace(/^http:/, 'ws:');
return url;
}
export async function newClient(channelID: string, closeCb: () => void, setScreenShareURL: (url: string) => void) {
let peer: any = null;
let stream: MediaStream;
let voiceTrackAdded = false;
let voiceTrack: MediaStreamTrack | null = null;
let isClosed = false;
const streams: MediaStream[] = [];
let stream: MediaStream;
let audioTrack: any;
try {
stream = await mediaDevices.getUserMedia({
video: false,
audio: true,
}) as MediaStream;
audioTrack = stream.getAudioTracks()[0];
audioTrack.enabled = false;
voiceTrack = stream.getAudioTracks()[0];
voiceTrack.enabled = false;
streams.push(stream);
} catch (err) {
console.log('Unable to get media device:', err); // eslint-disable-line no-console
}
const ws = new WebSocket(getWSConnectionURL(channelID));
const ws = new WebSocketClient(Client4.getWebSocketUrl());
const disconnect = () => {
ws.close();
if (!isClosed) {
ws.close();
}
streams.forEach((s) => {
s.getTracks().forEach((track: MediaStreamTrack) => {
@ -63,43 +64,66 @@ export async function newClient(channelID: string, closeCb: () => void, setScree
};
const mute = () => {
if (audioTrack) {
audioTrack.enabled = false;
if (!peer) {
return;
}
if (voiceTrackAdded) {
peer.replaceTrack(voiceTrack, null, stream);
}
if (voiceTrack) {
voiceTrack.enabled = false;
}
if (ws) {
ws.send(JSON.stringify({
type: 'mute',
}));
ws.send('mute');
}
};
const unmute = () => {
if (audioTrack) {
audioTrack.enabled = true;
if (!peer || !voiceTrack) {
return;
}
if (voiceTrackAdded) {
peer.replaceTrack(voiceTrack, voiceTrack, stream);
} else {
peer.addStream(stream);
voiceTrackAdded = true;
}
voiceTrack.enabled = true;
if (ws) {
ws.send(JSON.stringify({
type: 'unmute',
}));
ws.send('unmute');
}
};
ws.onerror = (err) => console.log('WS ERROR', err); // eslint-disable-line no-console
ws.on('error', (err) => {
console.log('WS (CALLS) ERROR', err); // eslint-disable-line no-console
ws.close();
});
ws.on('close', () => {
isClosed = true;
disconnect();
});
ws.on('join', async () => {
let config;
try {
config = await Client4.getCallsConfig();
} catch (err) {
console.log('ERROR FETCHING CALLS CONFIG:', err); // eslint-disable-line no-console
return;
}
ws.onopen = async () => {
InCallManager.start({media: 'audio'});
peer = new Peer(stream);
peer = new Peer(null, config.ICEServers);
peer.on('signal', (data: any) => {
if (data.type === 'offer' || data.type === 'answer') {
ws.send(JSON.stringify({
type: 'signal',
data,
}));
ws.send('sdp', {
data: deflate(JSON.stringify(data)),
}, true);
} else if (data.type === 'candidate') {
ws.send(JSON.stringify({
type: 'ice',
data,
}));
ws.send('ice', {
data: JSON.stringify(data.candidate),
});
}
});
@ -110,15 +134,23 @@ export async function newClient(channelID: string, closeCb: () => void, setScree
}
});
peer.on('error', (err: any) => console.log('PEER ERROR', err)); // eslint-disable-line no-console
peer.on('error', (err: any) => {
console.log('PEER ERROR', err); // eslint-disable-line no-console
});
});
ws.onmessage = ({data}) => {
const msg = JSON.parse(data);
if (msg.type === 'answer' || msg.type === 'offer') {
peer.signal(data);
}
};
};
ws.on('open', async () => {
ws.send('join', {
channelID,
});
});
ws.on('message', ({data}) => {
const msg = JSON.parse(data);
if (msg.type === 'answer' || msg.type === 'offer') {
peer.signal(data);
}
});
const waitForReady = () => {
const waitForReadyImpl = (callback: () => void, fail: () => void, timeout: number) => {
@ -127,7 +159,7 @@ export async function newClient(channelID: string, closeCb: () => void, setScree
return;
}
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
if (ws.state() === WebSocket.OPEN) {
callback();
} else {
waitForReadyImpl(callback, fail, timeout - 10);

View file

@ -69,7 +69,7 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
<ScrollView
alwaysBounceVertical={false}
contentContainerStyle={Object {}}
horizontal={true}
horizontal={false}
>
<Pressable
onPress={[Function]}
@ -132,7 +132,7 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
muted={true}
size="l"
userId="user-2-id"
volume={1}
volume={0}
/>
<Text
style={
@ -196,15 +196,15 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Leave"
id="call.leave"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Leave
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -232,15 +232,15 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Chat thread"
id="call.chat_thread"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Chat thread
</Text>
</Pressable>
<Pressable
style={
@ -267,15 +267,15 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Settings"
id="call.settings"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Settings
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -303,15 +303,15 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="More"
id="call.more"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
More
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -340,15 +340,15 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Unmute"
id="call.unmute"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Unmute
</Text>
</Pressable>
</View>
</View>
@ -445,21 +445,16 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="You are seing {userDisplayName} screen"
id="call.screen_share_user"
<Text
style={
Object {
"color": "white",
"margin": 3,
}
}
values={
Object {
"userDisplayName": "Someone",
}
}
/>
>
You are seeing User 2 screen
</Text>
</Pressable>
<View
style={
@ -511,15 +506,15 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Leave"
id="call.leave"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Leave
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -547,15 +542,15 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Chat thread"
id="call.chat_thread"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Chat thread
</Text>
</Pressable>
<Pressable
style={
@ -582,15 +577,15 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Settings"
id="call.settings"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Settings
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -618,15 +613,15 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="More"
id="call.more"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
More
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -655,15 +650,15 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Unmute"
id="call.unmute"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Unmute
</Text>
</Pressable>
</View>
</View>
@ -736,7 +731,7 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
<ScrollView
alwaysBounceVertical={false}
contentContainerStyle={Object {}}
horizontal={true}
horizontal={false}
>
<Pressable
onPress={[Function]}
@ -799,7 +794,7 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
muted={true}
size="l"
userId="user-2-id"
volume={1}
volume={0}
/>
<Text
style={
@ -850,15 +845,15 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Unmute"
id="call.unmute"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Unmute
</Text>
</Pressable>
<View
style={
@ -896,15 +891,15 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Leave"
id="call.leave"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Leave
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -932,15 +927,15 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Chat thread"
id="call.chat_thread"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Chat thread
</Text>
</Pressable>
<Pressable
style={
@ -967,15 +962,15 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Settings"
id="call.settings"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Settings
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -1003,15 +998,15 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="More"
id="call.more"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
More
</Text>
</Pressable>
</View>
</View>
@ -1147,7 +1142,7 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
muted={true}
size="m"
userId="user-2-id"
volume={1}
volume={0}
/>
<Text
style={
@ -1184,21 +1179,16 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="You are seing {userDisplayName} screen"
id="call.screen_share_user"
<Text
style={
Object {
"color": "white",
"margin": 3,
}
}
values={
Object {
"userDisplayName": "Someone",
}
}
/>
>
You are seeing User 2 screen
</Text>
</Pressable>
<View
style={
@ -1237,15 +1227,15 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Unmute"
id="call.unmute"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Unmute
</Text>
</Pressable>
<View
style={
@ -1283,15 +1273,15 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Leave"
id="call.leave"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Leave
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -1319,15 +1309,15 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Chat thread"
id="call.chat_thread"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Chat thread
</Text>
</Pressable>
<Pressable
style={
@ -1354,15 +1344,15 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Settings"
id="call.settings"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Settings
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -1390,15 +1380,15 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="More"
id="call.more"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
More
</Text>
</Pressable>
</View>
</View>
@ -1495,21 +1485,16 @@ exports[`CallScreen should show controls in landscape view on click the screen s
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="You are seing {userDisplayName} screen"
id="call.screen_share_user"
<Text
style={
Object {
"color": "white",
"margin": 3,
}
}
values={
Object {
"userDisplayName": "Someone",
}
}
/>
>
You are seeing User 2 screen
</Text>
</Pressable>
<View
style={
@ -1561,15 +1546,15 @@ exports[`CallScreen should show controls in landscape view on click the screen s
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Leave"
id="call.leave"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Leave
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -1597,15 +1582,15 @@ exports[`CallScreen should show controls in landscape view on click the screen s
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Chat thread"
id="call.chat_thread"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Chat thread
</Text>
</Pressable>
<Pressable
style={
@ -1632,15 +1617,15 @@ exports[`CallScreen should show controls in landscape view on click the screen s
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Settings"
id="call.settings"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Settings
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -1668,15 +1653,15 @@ exports[`CallScreen should show controls in landscape view on click the screen s
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="More"
id="call.more"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
More
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -1705,15 +1690,15 @@ exports[`CallScreen should show controls in landscape view on click the screen s
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Unmute"
id="call.unmute"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Unmute
</Text>
</Pressable>
</View>
</View>
@ -1790,7 +1775,7 @@ exports[`CallScreen should show controls in landscape view on click the users li
<ScrollView
alwaysBounceVertical={false}
contentContainerStyle={Object {}}
horizontal={true}
horizontal={false}
>
<Pressable
onPress={[Function]}
@ -1853,7 +1838,7 @@ exports[`CallScreen should show controls in landscape view on click the users li
muted={true}
size="l"
userId="user-2-id"
volume={1}
volume={0}
/>
<Text
style={
@ -1917,15 +1902,15 @@ exports[`CallScreen should show controls in landscape view on click the users li
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Leave"
id="call.leave"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Leave
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -1953,15 +1938,15 @@ exports[`CallScreen should show controls in landscape view on click the users li
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Chat thread"
id="call.chat_thread"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Chat thread
</Text>
</Pressable>
<Pressable
style={
@ -1988,15 +1973,15 @@ exports[`CallScreen should show controls in landscape view on click the users li
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Settings"
id="call.settings"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Settings
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -2024,15 +2009,15 @@ exports[`CallScreen should show controls in landscape view on click the users li
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="More"
id="call.more"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
More
</Text>
</Pressable>
<Pressable
onPress={[Function]}
@ -2061,15 +2046,15 @@ exports[`CallScreen should show controls in landscape view on click the users li
}
}
/>
<InjectIntl(FormattedText)
defaultMessage="Unmute"
id="call.unmute"
<Text
style={
Object {
"color": "#ffffff",
}
}
/>
>
Unmute
</Text>
</Pressable>
</View>
</View>

View file

@ -22,35 +22,38 @@ describe('CallScreen', () => {
id: 'user-1-id',
muted: false,
isTalking: false,
profile: {
id: 'user-1-id',
username: 'user-1-username',
nickname: 'User 1',
},
},
'user-2-id': {
id: 'user-2-id',
muted: true,
isTalking: true,
profile: {
id: 'user-2-id',
username: 'user-2-username',
nickname: 'User 2',
},
},
},
channelId: 'channel-id',
startTime: 100,
speakers: 'user-2-id',
screenOn: false,
screenOn: '',
threadId: false,
},
users: {
'user-1-id': {
id: 'user-1-id',
username: 'user-1-username',
nickname: 'User 1',
},
'user-2-id': {
id: 'user-2-id',
username: 'user-2-username',
nickname: 'User 2',
},
},
currentParticipant: {
id: 'user-2-id',
muted: true,
isTalking: true,
profile: {
id: 'user-2-id',
username: 'user-2-username',
nickname: 'User 2',
},
},
teammateNameDisplay: Preferences.DISPLAY_PREFER_NICKNAME,
screenShareURL: '',
@ -67,14 +70,14 @@ describe('CallScreen', () => {
});
test('should show controls in landscape view on click the users list', () => {
const props = {...baseProps, call: {...baseProps.call, screenOn: false}};
const props = {...baseProps, call: {...baseProps.call, screenOn: ''}};
const wrapper = shallow(<CallScreen {...props}/>);
wrapper.find({testID: 'users-list'}).simulate('press');
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should show controls in landscape view on click the screen share', () => {
const props = {...baseProps, call: {...baseProps.call, screenOn: true}, screenShareURL: 'screen-share-url'};
const props = {...baseProps, call: {...baseProps.call, screenOn: 'user-2-id'}, screenShareURL: 'screen-share-url'};
const wrapper = shallow(<CallScreen {...props}/>);
wrapper.find({testID: 'screen-share-container'}).simulate('press');
expect(wrapper.getElement()).toMatchSnapshot();
@ -105,7 +108,7 @@ describe('CallScreen', () => {
});
test('should match snapshot with screenshare', () => {
const props = {...baseProps, call: {...baseProps.call, screenOn: true}, screenShareURL: 'screen-share-url'};
const props = {...baseProps, call: {...baseProps.call, screenOn: 'user-2-id'}, screenShareURL: 'screen-share-url'};
const wrapper = shallow(<CallScreen {...props}/>);
expect(wrapper.getElement()).toMatchSnapshot();

View file

@ -2,12 +2,12 @@
// See LICENSE.txt for license information.
import React, {useEffect, useCallback, useState} from 'react';
import {Keyboard, View, Text, Platform, Pressable, SafeAreaView, ScrollView, useWindowDimensions} from 'react-native';
import {RTCView} from 'react-native-webrtc2';
import {Keyboard, View, Text, Platform, Pressable, SafeAreaView, ScrollView, useWindowDimensions, DeviceEventEmitter} from 'react-native';
import {RTCView} from 'react-native-webrtc';
import {showModalOverCurrentContext, mergeNavigationOptions, popTopScreen, goToScreen} from '@actions/navigation';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {WebsocketEvents} from '@constants';
import {THREAD} from '@constants/screen';
import {GenericAction} from '@mm-redux/types/actions';
import {displayUsername} from '@mm-redux/utils/user_utils';
@ -17,8 +17,7 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
import type {Theme} from '@mm-redux/types/theme';
import type {UserProfile} from '@mm-redux/types/users';
import type {IDMappedObjects} from '@mm-redux/types/utilities';
import type {Call, CallParticipant} from '@mmproducts/calls/store/types/calls';
import type {Call, CallParticipant, VoiceEventData} from '@mmproducts/calls/store/types/calls';
type Props = {
actions: {
@ -28,7 +27,6 @@ type Props = {
};
theme: Theme;
call: Call|null;
users: IDMappedObjects<UserProfile>;
currentParticipant: CallParticipant;
teammateNameDisplay: string;
screenShareURL: string;
@ -204,9 +202,6 @@ const getStyleSheet = makeStyleSheetFromTheme((props: any) => {
});
const CallScreen = (props: Props) => {
if (!props.call) {
return null;
}
const {width, height} = useWindowDimensions();
const isLandscape = width > height;
@ -224,6 +219,27 @@ const CallScreen = (props: Props) => {
});
}, []);
const [speaker, setSpeaker] = useState<UserProfile|null>(null);
const handleVoiceOn = (data: VoiceEventData) => {
if (data.channelId === props.call?.channelId) {
setSpeaker(props.call.participants[data.userId].profile);
}
};
const handleVoiceOff = (data: VoiceEventData) => {
if (data.channelId === props.call?.channelId && ((speaker?.id === data.userId) || !speaker)) {
setSpeaker(null);
}
};
useEffect(() => {
const onVoiceOn = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_ON, handleVoiceOn);
const onVoiceOff = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_OFF, handleVoiceOff);
return () => {
onVoiceOn.remove();
onVoiceOff.remove();
};
}, [props.call]);
const showOtherActions = () => {
const screen = 'CallOtherActions';
const passProps = {
@ -259,12 +275,16 @@ const CallScreen = (props: Props) => {
props.actions.muteMyself(props.call.channelId);
}
}
}, [props.call.channelId, props.currentParticipant]);
}, [props.call?.channelId, props.currentParticipant]);
const toggleControlsInLandscape = useCallback(() => {
setShowControlsInLandscape(!showControlsInLandscape);
}, [showControlsInLandscape]);
if (!props.call) {
return null;
}
let screenShareView = null;
if (props.screenShareURL && props.call.screenOn) {
screenShareView = (
@ -277,12 +297,9 @@ const CallScreen = (props: Props) => {
streamURL={props.screenShareURL}
style={style.screenShareImage}
/>
<FormattedText
id='call.screen_share_user'
defaultMessage='You are seing {userDisplayName} screen'
values={{userDisplayName: displayUsername(props.users[props.call.screenOn], props.teammateNameDisplay)}}
<Text
style={style.screenShareText}
/>
>{`You are seeing ${displayUsername(props.call.participants[props.call.screenOn].profile, props.teammateNameDisplay)} screen`}</Text>
</Pressable>
);
}
@ -307,11 +324,11 @@ const CallScreen = (props: Props) => {
>
<CallAvatar
userId={user.id}
volume={user.isTalking ? 1 : 0}
volume={speaker && speaker.id === user.id ? 1 : 0}
muted={user.muted}
size={props.call?.screenOn ? 'm' : 'l'}
/>
<Text style={style.username}>{displayUsername(props.users[user.id], props.teammateNameDisplay)}</Text>
<Text style={style.username}>{displayUsername(props.call?.participants[user.id].profile, props.teammateNameDisplay)}</Text>
</View>
);
})}
@ -354,17 +371,13 @@ const CallScreen = (props: Props) => {
style={style.muteIcon}
/>
{props.currentParticipant?.muted &&
<FormattedText
<Text
style={style.buttonText}
id='call.unmute'
defaultMessage='Unmute'
/>}
>{'Unmute'}</Text>}
{!props.currentParticipant?.muted &&
<FormattedText
<Text
style={style.buttonText}
id='call.mute'
defaultMessage='Mute'
/>}
>{'Mute'}</Text>}
</Pressable>}
<View style={style.otherButtons}>
<Pressable
@ -377,11 +390,9 @@ const CallScreen = (props: Props) => {
size={24}
style={{...style.buttonIcon, ...style.hangUpIcon}}
/>
<FormattedText
<Text
style={style.buttonText}
id='call.leave'
defaultMessage='Leave'
/>
>{'Leave'}</Text>
</Pressable>
<Pressable
style={style.button}
@ -392,11 +403,9 @@ const CallScreen = (props: Props) => {
size={24}
style={style.buttonIcon}
/>
<FormattedText
<Text
style={style.buttonText}
id='call.chat_thread'
defaultMessage='Chat thread'
/>
>{'Chat thread'}</Text>
</Pressable>
<Pressable
style={style.button}
@ -406,11 +415,9 @@ const CallScreen = (props: Props) => {
size={24}
style={style.buttonIcon}
/>
<FormattedText
<Text
style={style.buttonText}
id='call.settings'
defaultMessage='Settings'
/>
>{'Settings'}</Text>
</Pressable>
<Pressable
style={style.button}
@ -421,11 +428,9 @@ const CallScreen = (props: Props) => {
size={24}
style={style.buttonIcon}
/>
<FormattedText
<Text
style={style.buttonText}
id='call.more'
defaultMessage='More'
/>
>{'More'}</Text>
</Pressable>
{isLandscape &&
<Pressable
@ -439,17 +444,13 @@ const CallScreen = (props: Props) => {
style={{...style.buttonIcon, ...style.muteIconLandscape}}
/>
{props.currentParticipant?.muted &&
<FormattedText
<Text
style={style.buttonText}
id='call.unmute'
defaultMessage='Unmute'
/>}
>{'Unmute'}</Text>}
{!props.currentParticipant?.muted &&
<FormattedText
<Text
style={style.buttonText}
id='call.mute'
defaultMessage='Mute'
/>}
>{'Mute'}</Text>}
</Pressable>}
</View>
</View>

View file

@ -19,7 +19,6 @@ function mapStateToProps(state: GlobalState) {
theme: getTheme(state),
call: currentCall,
teammateNameDisplay: getTeammateNameDisplaySetting(state),
users: state.entities.users.profiles,
currentParticipant: currentCall && currentCall.participants[currentUserId],
screenShareURL: getScreenShareURL(state),
};

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {injectIntl} from 'react-intl';
import {View} from 'react-native';
import {dismissModal} from '@actions/navigation';
@ -13,10 +13,9 @@ import type {Theme} from '@mm-redux/types/theme';
type Props = {
theme: Theme;
intl: typeof intlShape;
}
const CallOtherActions = ({theme, intl}: Props) => {
const CallOtherActions = ({theme}: Props) => {
const close = () => {
dismissModal();
};
@ -41,21 +40,21 @@ const CallOtherActions = ({theme, intl}: Props) => {
destructive={false}
icon='account-plus-outline'
onPress={addParticipants}
text={intl.formatMessage({id: 'call.add_participants', defaultMessage: 'Add participants'})}
text='Add participants'
theme={theme}
/>
<Action
destructive={false}
icon='link-variant'
onPress={copyCallLink}
text={intl.formatMessage({id: 'call.copy_call_link', defaultMessage: 'Copy call link'})}
text='Copy call link'
theme={theme}
/>
<Action
destructive={false}
icon='send-outline'
onPress={giveFeedback}
text={intl.formatMessage({id: 'call.give_feedback', defaultMessage: 'Give Feedback'})}
text='Give Feedback'
theme={theme}
/>
</SlideUpPanel>

View file

@ -18,7 +18,7 @@ import {
RTCSessionDescriptionType,
MessageEvent,
RTCIceCandidateType,
} from 'react-native-webrtc2';
} from 'react-native-webrtc';
import stream from 'readable-stream';
const queueMicrotask = (callback: any) => {
@ -94,7 +94,7 @@ export default class Peer extends stream.Duplex {
private pc: RTCPeerConnection|null = null;
private onFinishBound?: () => void;
constructor(localStream: MediaStream) {
constructor(localStream: MediaStream | null, iceServers?: string[]) {
super({allowHalfOpen: false});
this.streams = localStream ? [localStream] : [];
@ -103,18 +103,24 @@ export default class Peer extends stream.Duplex {
this.onFinish();
};
const connConfig = {
iceServers: [
{
urls: [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478',
],
},
],
sdpSemantics: 'unified-plan',
};
if (iceServers && iceServers.length > 0) {
connConfig.iceServers[0].urls = iceServers;
}
try {
this.pc = new RTCPeerConnection({
iceServers: [
{
urls: [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478',
],
},
],
sdpSemantics: 'unified-plan',
});
this.pc = new RTCPeerConnection(connConfig);
} catch (err) {
this.destroy(errCode(err, 'ERR_PC_CONSTRUCTOR'));
return;
@ -318,11 +324,11 @@ export default class Peer extends stream.Duplex {
* @param {MediaStreamTrack} track
* @param {MediaStream} s
*/
addTrack(track: MediaStreamTrack, s: MediaStream) {
async addTrack(track: MediaStreamTrack, s: MediaStream) {
if (this.destroying) {
return;
}
if (this.destroyed) {
if (this.destroyed || !this.pc) {
throw errCode(
new Error('cannot addTrack after peer is destroyed'),
'ERR_DESTROYED',
@ -330,10 +336,11 @@ export default class Peer extends stream.Duplex {
}
const submap = this.senderMap.get(track) || new Map(); // nested Maps map [track, stream] to sender
let sender = submap.get(s);
const sender = submap.get(s);
if (!sender) {
sender = s.addTrack(track);
submap.set(s, sender);
const transceiver = await this.pc.addTransceiver(track, {direction: 'sendrecv'}) as any;
/* eslint-disable no-underscore-dangle */
submap.set(s, transceiver._sender);
this.senderMap.set(track, submap);
this.needsNegotiation();
} else if (sender.removed) {
@ -351,6 +358,36 @@ export default class Peer extends stream.Duplex {
}
}
/**
* Replace a MediaStreamTrack by another in the connection.
* @param {MediaStreamTrack} oldTrack
* @param {MediaStreamTrack} newTrack
* @param {MediaStream} stream
*/
replaceTrack(oldTrack: MediaStreamTrack, newTrack: MediaStreamTrack | null, s: MediaStream) {
if (this.destroying) {
return;
}
if (this.destroyed) {
throw errCode(new Error('cannot replaceTrack after peer is destroyed'), 'ERR_DESTROYED');
}
const submap = this.senderMap.get(oldTrack);
const sender = submap ? submap.get(s) : null;
if (!sender) {
throw errCode(new Error('Cannot replace track that was never added.'), 'ERR_TRACK_NOT_ADDED');
}
if (newTrack) {
this.senderMap.set(newTrack, submap);
}
if (sender.replaceTrack == null) {
this.destroy(errCode(new Error('replaceTrack is not supported in this browser'), 'ERR_UNSUPPORTED_REPLACETRACK'));
} else {
sender.replaceTrack(newTrack);
}
}
needsNegotiation() {
if (this.batchedNegotiation) {
return;

View file

@ -31,8 +31,9 @@ export function loadCalls(): ActionFunc {
if (channel.call) {
callsResults[channel.channel_id] = {
participants: channel.call.users.reduce((prev: Dictionary<CallParticipant>, cur: string, curIdx: number) => {
const profile = getState().entities.users.profiles[cur];
const muted = channel.call.states && channel.call.states[curIdx] ? !channel.call.states[curIdx].unmuted : true;
prev[cur] = {id: cur, muted, isTalking: false};
prev[cur] = {id: cur, muted, isTalking: false, profile};
return prev;
}, {}),
channelId: channel.channel_id,

View file

@ -1,7 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {GenericAction} from '@mm-redux/types/actions';
import {DeviceEventEmitter} from 'react-native';
import {WebsocketEvents} from '@constants';
import {getMissingProfilesByIds} from '@mm-redux/actions/users';
import {GenericAction, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
import {WebSocketMessage} from '@mm-redux/types/websocket';
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
@ -12,11 +16,13 @@ export function handleCallUserDisconnected(msg: WebSocketMessage): GenericAction
};
}
export function handleCallUserConnected(msg: WebSocketMessage): GenericAction {
return {
export async function handleCallUserConnected(dispatch: DispatchFunc, getState: GetStateFunc, msg: WebSocketMessage) {
await dispatch(getMissingProfilesByIds([msg.data.userID]));
const profile = getState().entities.users.profiles[msg.data.userID];
dispatch({
type: CallsTypes.RECEIVED_JOINED_CALL,
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
};
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID, profile},
});
}
export function handleCallUserMuted(msg: WebSocketMessage): GenericAction {
@ -33,18 +39,12 @@ export function handleCallUserUnmuted(msg: WebSocketMessage): GenericAction {
};
}
export function handleCallUserVoiceOn(msg: WebSocketMessage): GenericAction {
return {
type: CallsTypes.RECEIVED_VOICE_ON_USER_CALL,
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
};
export function handleCallUserVoiceOn(msg: WebSocketMessage) {
DeviceEventEmitter.emit(WebsocketEvents.CALLS_USER_VOICE_ON, {channelId: msg.broadcast.channel_id, userId: msg.data.userID});
}
export function handleCallUserVoiceOff(msg: WebSocketMessage): GenericAction {
return {
type: CallsTypes.RECEIVED_VOICE_OFF_USER_CALL,
data: {channelId: msg.broadcast.channel_id, userId: msg.data.userID},
};
export function handleCallUserVoiceOff(msg: WebSocketMessage) {
DeviceEventEmitter.emit(WebsocketEvents.CALLS_USER_VOICE_OFF, {channelId: msg.broadcast.channel_id, userId: msg.data.userID});
}
export function handleCallStarted(msg: WebSocketMessage): GenericAction {

View file

@ -10,8 +10,8 @@ import callsReducer from './calls';
describe('Reducers.calls.calls', () => {
const call1 = {
participants: {
'user-1': {id: 'user-1', muted: false, isTalking: false},
'user-2': {id: 'user-2', muted: true, isTalking: true},
'user-1': {id: 'user-1', muted: false, isTalking: false, profile: {id: 'user-1'}},
'user-2': {id: 'user-2', muted: true, isTalking: true, profile: {id: 'user-2'}},
},
channelId: 'channel-1',
startTime: 123,
@ -21,8 +21,8 @@ describe('Reducers.calls.calls', () => {
};
const call2 = {
participants: {
'user-3': {id: 'user-3', muted: false, isTalking: false},
'user-4': {id: 'user-4', muted: true, isTalking: true},
'user-3': {id: 'user-3', muted: false, isTalking: false, profile: {id: 'user-3'}},
'user-4': {id: 'user-4', muted: true, isTalking: true, profile: {id: 'user-4'}},
},
channelId: 'channel-2',
startTime: 123,
@ -32,8 +32,8 @@ describe('Reducers.calls.calls', () => {
};
const call3 = {
participants: {
'user-5': {id: 'user-5', muted: false, isTalking: false},
'user-6': {id: 'user-6', muted: true, isTalking: true},
'user-5': {id: 'user-5', muted: false, isTalking: false, profile: {id: 'user-5'}},
'user-6': {id: 'user-6', muted: true, isTalking: true, profile: {id: 'user-6'}},
},
channelId: 'channel-3',
startTime: 123,
@ -71,7 +71,7 @@ describe('Reducers.calls.calls', () => {
{
'channel-1': {
participants: {
'user-2': {id: 'user-2', muted: true, isTalking: true},
'user-2': {id: 'user-2', muted: true, isTalking: true, profile: {id: 'user-2'}},
},
channelId: 'channel-1',
startTime: 123,
@ -97,7 +97,7 @@ describe('Reducers.calls.calls', () => {
const initialState = {calls: {'channel-1': call1}};
const testAction = {
type: CallsTypes.RECEIVED_JOINED_CALL,
data: {channelId: 'channel-1', userId: 'user-3'},
data: {channelId: 'channel-1', userId: 'user-3', profile: {id: 'user-3'}},
};
let state = callsReducer(initialState, testAction);
assert.deepEqual(
@ -105,9 +105,9 @@ describe('Reducers.calls.calls', () => {
{
'channel-1': {
participants: {
'user-1': {id: 'user-1', muted: false, isTalking: false},
'user-2': {id: 'user-2', muted: true, isTalking: true},
'user-3': {id: 'user-3', muted: true, isTalking: false},
'user-1': {id: 'user-1', muted: false, isTalking: false, profile: {id: 'user-1'}},
'user-2': {id: 'user-2', muted: true, isTalking: true, profile: {id: 'user-2'}},
'user-3': {id: 'user-3', muted: true, isTalking: false, profile: {id: 'user-3'}},
},
channelId: 'channel-1',
startTime: 123,
@ -118,7 +118,7 @@ describe('Reducers.calls.calls', () => {
},
);
testAction.data = {channelId: 'invalid-channel', userId: 'user-1'};
testAction.data = {channelId: 'invalid-channel', userId: 'user-1', profile: {id: 'user-1'}};
state = callsReducer(initialState, testAction);
assert.deepEqual(state.calls, {'channel-1': call1});
@ -179,44 +179,6 @@ describe('Reducers.calls.calls', () => {
state = callsReducer(initialState, testAction);
assert.deepEqual(state.calls, initialState.calls);
});
it('RECEIVED_VOICE_ON_USER_CALL', async () => {
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
const testAction = {
type: CallsTypes.RECEIVED_VOICE_ON_USER_CALL,
data: {channelId: 'channel-1', userId: 'user-1'},
};
let state = callsReducer(initialState, testAction);
assert.equal(state.calls['channel-1'].participants['user-1'].isTalking, true);
assert.deepEqual(state.calls['channel-1'].speakers, ['user-1', 'user-2']);
testAction.data = {channelId: 'channel-1', userId: 'invalidUser'};
state = callsReducer(initialState, testAction);
assert.deepEqual(state.calls, initialState.calls);
testAction.data = {channelId: 'invalid-channel', userId: 'user-2'};
state = callsReducer(initialState, testAction);
assert.deepEqual(state.calls, initialState.calls);
});
it('RECEIVED_VOICE_OFF_USER_CALL', async () => {
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
const testAction = {
type: CallsTypes.RECEIVED_VOICE_OFF_USER_CALL,
data: {channelId: 'channel-1', userId: 'user-2'},
};
let state = callsReducer(initialState, testAction);
assert.equal(state.calls['channel-1'].participants['user-2'].isTalking, false);
assert.deepEqual(state.calls['channel-1'].speakers, []);
testAction.data = {channelId: 'channel-1', userId: 'invalidUser'};
state = callsReducer(initialState, testAction);
assert.deepEqual(state.calls, initialState.calls);
testAction.data = {channelId: 'invalid-channel', userId: 'user-2'};
state = callsReducer(initialState, testAction);
assert.deepEqual(state.calls, initialState.calls);
});
it('RECEIVED_CHANNEL_CALL_SCREEN_ON', async () => {
const initialState = {calls: {'channel-1': call1, 'channel-2': call2}};
const testAction = {

View file

@ -31,7 +31,7 @@ function calls(state: Dictionary<Call> = {}, action: GenericAction) {
return nextState;
}
case CallsTypes.RECEIVED_JOINED_CALL: {
const {channelId, userId} = action.data;
const {channelId, userId, profile} = action.data;
if (!state[channelId]) {
return state;
}
@ -40,6 +40,7 @@ function calls(state: Dictionary<Call> = {}, action: GenericAction) {
id: userId,
muted: true,
isTalking: false,
profile,
};
const nextState = {...state};
nextState[channelId] = channelUpdate;
@ -87,41 +88,6 @@ function calls(state: Dictionary<Call> = {}, action: GenericAction) {
nextState[channelId] = channelUpdate;
return nextState;
}
case CallsTypes.RECEIVED_VOICE_ON_USER_CALL: {
const {channelId, userId} = action.data;
if (!state[channelId]) {
return state;
}
if (!state[channelId].participants[userId]) {
return state;
}
const userUpdate = {...state[channelId].participants[userId], isTalking: true};
const channelUpdate = {...state[channelId], participants: {...state[channelId].participants}};
channelUpdate.participants[userId] = userUpdate;
channelUpdate.speakers = [userId, ...(channelUpdate.speakers || [])];
const nextState = {...state};
nextState[channelId] = channelUpdate;
return nextState;
}
case CallsTypes.RECEIVED_VOICE_OFF_USER_CALL: {
const {channelId, userId} = action.data;
if (!state[channelId]) {
return state;
}
if (!state[channelId].participants[userId]) {
return state;
}
const userUpdate = {...state[channelId].participants[userId], isTalking: false};
const channelUpdate = {...state[channelId], participants: {...state[channelId].participants}};
channelUpdate.participants[userId] = userUpdate;
channelUpdate.speakers = channelUpdate.speakers?.filter((id) => id !== userId);
if (!channelUpdate.speakers) {
channelUpdate.speakers = [];
}
const nextState = {...state};
nextState[channelId] = channelUpdate;
return nextState;
}
case CallsTypes.RECEIVED_CHANNEL_CALL_SCREEN_ON: {
const {channelId, userId} = action.data;
if (!state[channelId]) {

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {UserProfile} from '@mm-redux/types/users';
import {Dictionary} from '@mm-redux/types/utilities';
export type CallsState = {
@ -23,6 +24,7 @@ export type CallParticipant = {
id: string;
muted: boolean;
isTalking: boolean;
profile: UserProfile;
}
export type ServerChannelState = {
@ -44,3 +46,8 @@ export type ServerCallState = {
thread_id: string;
screen_sharing_id: string;
}
export type VoiceEventData = {
channelId: string;
userId: string;
}

View file

@ -0,0 +1,100 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {EventEmitter} from 'events';
import {encode} from '@msgpack/msgpack/dist';
export default class WebSocketClient extends EventEmitter {
private ws: WebSocket | null;
private seqNo = 0;
private connID = '';
private eventPrefix = 'custom_com.mattermost.calls';
constructor(connURL: string) {
super();
this.ws = new WebSocket(connURL);
this.ws.onerror = (err) => {
this.emit('error', err);
this.ws = null;
this.close();
};
this.ws.onclose = () => {
this.ws = null;
this.close();
};
this.ws.onmessage = ({data}) => {
if (!data) {
return;
}
let msg;
try {
msg = JSON.parse(data);
} catch (err) {
console.log(err); // eslint-disable-line no-console
}
if (!msg || !msg.event || !msg.data) {
return;
}
if (msg.event === 'hello') {
this.connID = msg.data.connection_id;
this.emit('open');
return;
} else if (!this.connID) {
return;
}
if (msg.data.connID !== this.connID) {
return;
}
if (msg.event === this.eventPrefix + '_join') {
this.emit('join');
}
if (msg.event === this.eventPrefix + '_error') {
this.emit('error', msg.data);
}
if (msg.event === this.eventPrefix + '_signal') {
this.emit('message', msg.data);
}
};
}
send(action: string, data?: Object, binary?: boolean) {
const msg = {
action: `${this.eventPrefix}_${action}`,
seq: this.seqNo++,
data,
};
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
if (binary) {
this.ws.send(encode(msg));
} else {
this.ws.send(JSON.stringify(msg));
}
}
}
close() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.seqNo = 0;
this.connID = '';
this.emit('close');
}
state() {
if (!this.ws) {
return WebSocket.CLOSED;
}
return this.ws.readyState;
}
}

View file

@ -51,6 +51,7 @@ export default class ChannelInfo extends PureComponent {
currentUserId: PropTypes.string,
isTeammateGuest: PropTypes.bool.isRequired,
isDirectMessage: PropTypes.bool.isRequired,
isGroupMessage: PropTypes.bool.isRequired,
teammateId: PropTypes.string,
theme: PropTypes.object.isRequired,
customStatus: PropTypes.object,
@ -116,7 +117,7 @@ export default class ChannelInfo extends PureComponent {
};
actionsRows = (channelIsArchived) => {
const {currentChannel, currentUserId, isDirectMessage, theme, isCallsEnabled, callsFeatureEnabled, isChannelAdmin} = this.props;
const {currentChannel, currentUserId, isDirectMessage, isGroupMessage, theme, isCallsEnabled, callsFeatureEnabled, isChannelAdmin} = this.props;
if (channelIsArchived) {
return (
@ -191,7 +192,7 @@ export default class ChannelInfo extends PureComponent {
testID='channel_info.start_call.action'
theme={theme}
onPress={this.toggleCalls}
canEnableDisableCalls={isChannelAdmin}
canEnableDisableCalls={isDirectMessage || isGroupMessage || isChannelAdmin}
enabled={isCallsEnabled}
/>
</>}

View file

@ -56,7 +56,8 @@ function makeMapStateToProps() {
customStatusExpirySupported = customStatusEnabled ? isCustomStatusExpirySupported(state) : false;
}
if (currentChannel.type === General.GM_CHANNEL) {
const isGroupMessage = currentChannel.type === General.GM_CHANNEL;
if (isGroupMessage) {
currentChannelMemberCount = currentChannel.display_name.split(',').length;
}
@ -70,6 +71,7 @@ function makeMapStateToProps() {
currentUserId,
isTeammateGuest,
isDirectMessage,
isGroupMessage,
teammateId,
theme: getTheme(state),
customStatus,

View file

@ -57,27 +57,6 @@
"apps.suggestion.no_static": "No matching options.",
"apps.suggestion.no_suggestion": "No matching suggestions.",
"archivedChannelMessage": "You are viewing an **archived channel**. New messages cannot be posted.",
"call_message.call_ended": "Call ended",
"call_message.call_ended_at": "Ended at {time}",
"call_message.call_lasted": "Lasted {duration}",
"call_message.current_call": "Current Call",
"call_message.join_call": "Join Call",
"call_message.started_a_call": "{user} started a call",
"call.add_participants": "Add participants",
"call.chat_thread": "Chat thread",
"call.copy_call_link": "Copy call link",
"call.give_feedback": "Give Feedback",
"call.leave": "Leave",
"call.more": "More",
"call.mute": "Mute",
"call.screen_share_user": "You are seing {userDisplayName} screen",
"call.settings": "Settings",
"call.unmute": "Unmute",
"calls.confirm-to-join-cancel": "Cancel",
"calls.confirm-to-join-description": "You are already on a channel call in ~{callChannelName}. Do you want to leave your current call and join the call in ~{currentChannelName}?",
"calls.confirm-to-join-leave-and-join": "Leave & Join",
"calls.confirm-to-join-title": "Are you sure you want to switch to a different call?",
"calls.join_call.participants_list_header": "CALL PARTICIPANTS",
"camera_type.photo.option": "Capture Photo",
"camera_type.title": "Choose an action",
"camera_type.video.option": "Record Video",
@ -156,8 +135,6 @@
"create_comment.addComment": "Add a comment...",
"create_post.deactivated": "You are viewing an archived channel with a deactivated user.",
"create_post.write": "Write to {channelDisplayName}",
"current_call.channel-name": "~{channelName}",
"current_call.user-is-speaking": "{username} is speaking",
"custom_status.expiry_dropdown.custom": "Custom",
"custom_status.expiry_dropdown.date_and_time": "Date and Time",
"custom_status.expiry_dropdown.dont_clear": "Don't clear",
@ -318,13 +295,9 @@
"mobile.channel_info.copy_header": "Copy Header",
"mobile.channel_info.copy_purpose": "Copy Purpose",
"mobile.channel_info.delete_failed": "We couldn't archive the channel {displayName}. Please check your connection and try again.",
"mobile.channel_info.disable_calls": "Disable Calls",
"mobile.channel_info.edit": "Edit Channel",
"mobile.channel_info.enable_calls": "Enable Calls",
"mobile.channel_info.join_ongoing_call": "Join Ongoing Call",
"mobile.channel_info.privateChannel": "Private Channel",
"mobile.channel_info.publicChannel": "Public Channel",
"mobile.channel_info.start_call": "Start Call",
"mobile.channel_info.unarchive_failed": "We couldn't unarchive the channel {displayName}. Please check your connection and try again.",
"mobile.channel_list.alertNo": "No",
"mobile.channel_list.alertYes": "Yes",

View file

@ -276,7 +276,7 @@ PODS:
- react-native-video/Video (= 5.2.0)
- react-native-video/Video (5.2.0):
- React-Core
- react-native-webrtc2 (1.84.5):
- react-native-webrtc (1.75.3):
- React
- react-native-webview (11.15.0):
- React-Core
@ -492,7 +492,7 @@ DEPENDENCIES:
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-startup-time (from `../node_modules/react-native-startup-time`)
- react-native-video (from `../node_modules/react-native-video`)
- react-native-webrtc2 (from `../node_modules/react-native-webrtc2`)
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
@ -631,8 +631,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-startup-time"
react-native-video:
:path: "../node_modules/react-native-video"
react-native-webrtc2:
:path: "../node_modules/react-native-webrtc2"
react-native-webrtc:
:path: "../node_modules/react-native-webrtc"
react-native-webview:
:path: "../node_modules/react-native-webview"
React-perflogger:
@ -766,7 +766,7 @@ SPEC CHECKSUMS:
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
react-native-startup-time: 1a068b744ce5097a85ebe0fbff691b05961c324d
react-native-video: a4c2635d0802f983594b7057e1bce8f442f0ad28
react-native-webrtc2: b3f1d5504c8bbd3aa4ce9b6c3ce0fbc47c7e146a
react-native-webrtc: 86d841823e66d68cc1f86712db1c2956056bf0c2
react-native-webview: e89bf2dba26a04cda967814df3ed1be99f291233
React-perflogger: 93075d8931c32cd1fce8a98c15d2d5ccc4d891bd
React-RCTActionSheet: 7d3041e6761b4f3044a37079ddcb156575fb6d89

93
package-lock.json generated
View file

@ -5,11 +5,13 @@
"requires": true,
"packages": {
"": {
"name": "mattermost-mobile",
"version": "1.48.1",
"hasInstallScript": true,
"license": "Apache 2.0",
"dependencies": {
"@mattermost/react-native-paste-input": "0.3.5",
"@msgpack/msgpack": "2.7.1",
"@react-native-async-storage/async-storage": "1.15.14",
"@react-native-community/cameraroll": "4.1.2",
"@react-native-community/clipboard": "1.5.1",
@ -37,6 +39,7 @@
"jail-monkey": "2.6.0",
"mime-db": "1.51.0",
"moment-timezone": "0.5.34",
"pako": "2.0.4",
"prop-types": "15.8.0",
"react": "17.0.2",
"react-intl": "2.8.0",
@ -79,7 +82,7 @@
"react-native-svg": "12.1.1",
"react-native-vector-icons": "9.0.0",
"react-native-video": "5.2.0",
"react-native-webrtc2": "1.84.5",
"react-native-webrtc": "github:streamer45/react-native-webrtc",
"react-native-webview": "11.15.0",
"react-native-youtube": "2.0.2",
"react-redux": "7.2.6",
@ -2715,6 +2718,14 @@
"react-native": "*"
}
},
"node_modules/@msgpack/msgpack": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.7.1.tgz",
"integrity": "sha512-ApwiSL2c9ObewdOE/sqt788P1C5lomBOHyO8nUBCr4ofErBCnYQ003NtJ8lS9OQZc11ximkbmgAZJjB8y6cCdA==",
"engines": {
"node": ">= 10"
}
},
"node_modules/@nicolo-ribaudo/chokidar-2": {
"version": "2.1.8-no-fsevents.3",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz",
@ -6450,6 +6461,13 @@
"pako": "~1.0.5"
}
},
"node_modules/browserify-zlib/node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true,
"peer": true
},
"node_modules/browserslist": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.18.1.tgz",
@ -17401,7 +17419,6 @@
"node_modules/mmjstool": {
"version": "1.0.0",
"resolved": "git+ssh://git@github.com/mattermost/mattermost-utilities.git#3faa6075089a541d8c90ed85114e644c7a23fedf",
"integrity": "sha512-84xbA6yRt+pEbYk/OmM9sbJQp7PkSAgqvn0N/QXuj1RKR+upxdAa6vTbTTBA+XweECN6xebCWncGJJiqWrtR1g==",
"dev": true,
"dependencies": {
"estree-walk": "2.2.0",
@ -19230,11 +19247,9 @@
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true,
"peer": true
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
},
"node_modules/parallel-transform": {
"version": "1.2.0",
@ -20743,10 +20758,23 @@
"shaka-player": "^2.5.9"
}
},
"node_modules/react-native-webrtc2": {
"version": "1.84.5",
"resolved": "https://registry.npmjs.org/react-native-webrtc2/-/react-native-webrtc2-1.84.5.tgz",
"integrity": "sha512-3M777RkkjQGnmDu0LrpFxfxmu8SRFfJqozxlBMPMpi91aoqQ77Ut/BH3xYnrYs3vWx/VtZdPYOxYCfrumI8WiA=="
"node_modules/react-native-webrtc": {
"version": "1.75.3",
"resolved": "git+ssh://git@github.com/streamer45/react-native-webrtc.git#7fe7d434892e6b29c5a6086d30b10f482db1e592",
"dependencies": {
"base64-js": "^1.1.2",
"event-target-shim": "^1.0.5",
"prop-types": "^15.5.10",
"uuid": "^3.3.2"
},
"peerDependencies": {
"react-native": ">=0.40.0"
}
},
"node_modules/react-native-webrtc/node_modules/event-target-shim": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-1.1.1.tgz",
"integrity": "sha1-qG5e5r2qFgVEddp5fM3fDFVphJE="
},
"node_modules/react-native-webview": {
"version": "11.15.0",
@ -28089,6 +28117,11 @@
"requires": {
}
},
"@msgpack/msgpack": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.7.1.tgz",
"integrity": "sha512-ApwiSL2c9ObewdOE/sqt788P1C5lomBOHyO8nUBCr4ofErBCnYQ003NtJ8lS9OQZc11ximkbmgAZJjB8y6cCdA=="
},
"@nicolo-ribaudo/chokidar-2": {
"version": "2.1.8-no-fsevents.3",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz",
@ -31097,6 +31130,15 @@
"peer": true,
"requires": {
"pako": "~1.0.5"
},
"dependencies": {
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true,
"peer": true
}
}
},
"browserslist": {
@ -39755,7 +39797,6 @@
},
"mmjstool": {
"version": "git+ssh://git@github.com/mattermost/mattermost-utilities.git#3faa6075089a541d8c90ed85114e644c7a23fedf",
"integrity": "sha512-84xbA6yRt+pEbYk/OmM9sbJQp7PkSAgqvn0N/QXuj1RKR+upxdAa6vTbTTBA+XweECN6xebCWncGJJiqWrtR1g==",
"dev": true,
"from": "mmjstool@git://github.com/mattermost/mattermost-utilities#3faa6075089a541d8c90ed85114e644c7a23fedf",
"requires": {
@ -41188,11 +41229,9 @@
}
},
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true,
"peer": true
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
},
"parallel-transform": {
"version": "1.2.0",
@ -42493,10 +42532,22 @@
"shaka-player": "^2.5.9"
}
},
"react-native-webrtc2": {
"version": "1.84.5",
"resolved": "https://registry.npmjs.org/react-native-webrtc2/-/react-native-webrtc2-1.84.5.tgz",
"integrity": "sha512-3M777RkkjQGnmDu0LrpFxfxmu8SRFfJqozxlBMPMpi91aoqQ77Ut/BH3xYnrYs3vWx/VtZdPYOxYCfrumI8WiA=="
"react-native-webrtc": {
"version": "git+ssh://git@github.com/streamer45/react-native-webrtc.git#7fe7d434892e6b29c5a6086d30b10f482db1e592",
"from": "react-native-webrtc@github:streamer45/react-native-webrtc",
"requires": {
"base64-js": "^1.1.2",
"event-target-shim": "^1.0.5",
"prop-types": "^15.5.10",
"uuid": "^3.3.2"
},
"dependencies": {
"event-target-shim": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-1.1.1.tgz",
"integrity": "sha1-qG5e5r2qFgVEddp5fM3fDFVphJE="
}
}
},
"react-native-webview": {
"version": "11.15.0",

View file

@ -8,6 +8,7 @@
"private": true,
"dependencies": {
"@mattermost/react-native-paste-input": "0.3.5",
"@msgpack/msgpack": "2.7.1",
"@react-native-async-storage/async-storage": "1.15.14",
"@react-native-community/cameraroll": "4.1.2",
"@react-native-community/clipboard": "1.5.1",
@ -35,6 +36,7 @@
"jail-monkey": "2.6.0",
"mime-db": "1.51.0",
"moment-timezone": "0.5.34",
"pako": "2.0.4",
"prop-types": "15.8.0",
"react": "17.0.2",
"react-intl": "2.8.0",
@ -77,7 +79,7 @@
"react-native-svg": "12.1.1",
"react-native-vector-icons": "9.0.0",
"react-native-video": "5.2.0",
"react-native-webrtc2": "1.84.5",
"react-native-webrtc": "github:streamer45/react-native-webrtc",
"react-native-webview": "11.15.0",
"react-native-youtube": "2.0.2",
"react-redux": "7.2.6",

View file

@ -8,7 +8,7 @@
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
declare module 'react-native-webrtc2' {
declare module 'react-native-webrtc' {
export const RTCView: any;
export type RTCSignalingState =
| 'stable'
@ -162,6 +162,8 @@ declare module 'react-native-webrtc2' {
addStream(stream: MediaStream): void;
addTrack(track: MediaStreamTrack): void;
addTransceiver(kind: 'audio'|'video'|MediaStreamTrack, init: any): void;
removeStream(stream: MediaStream): void;