diff --git a/NOTICE.txt b/NOTICE.txt
index 3b7747d05..bd897b68b 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -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
diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts
index 5e3891edb..5b994f6ce 100644
--- a/app/actions/websocket/index.ts
+++ b/app/actions/websocket/index.ts
@@ -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:
diff --git a/app/products/calls/client/rest.ts b/app/products/calls/client/rest.ts
index c9cd0ead0..cbecb6054 100644
--- a/app/products/calls/client/rest.ts
+++ b/app/products/calls/client/rest.ts
@@ -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}`,
diff --git a/app/products/calls/components/__snapshots__/enable_disable_calls.test.js.snap b/app/products/calls/components/__snapshots__/enable_disable_calls.test.js.snap
index b6deb6db0..204cdcaa3 100644
--- a/app/products/calls/components/__snapshots__/enable_disable_calls.test.js.snap
+++ b/app/products/calls/components/__snapshots__/enable_disable_calls.test.js.snap
@@ -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",
diff --git a/app/products/calls/components/call_avatar.tsx b/app/products/calls/components/call_avatar.tsx
index da9893efd..d87b99a3f 100644
--- a/app/products/calls/components/call_avatar.tsx
+++ b/app/products/calls/components/call_avatar.tsx
@@ -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) => {
-
+ {props.userId ?
+ :
+
+ }
{props.muted !== undefined &&
-
+ >
+ User 1 started a call
+
-
+ >
+ Join call
+
`;
@@ -129,16 +124,16 @@ exports[`CallMessage should match snapshot for ended call 1`] = `
}
}
>
-
+ >
+ Call ended
+
- ,
- }
- }
+ >
+ Ended at
+
+
•
-
+ >
+ Lasted a few seconds
+
@@ -230,21 +216,16 @@ exports[`CallMessage should match snapshot for the call already in the current c
}
}
>
-
+ >
+ User 1 started a call
+
-
+ >
+ Current call
+
`;
diff --git a/app/products/calls/components/call_message/call_message.tsx b/app/products/calls/components/call_message/call_message.tsx
index 1f0beb050..94ccd7aa2 100644
--- a/app/products/calls/components/call_message/call_message.tsx
+++ b/app/products/calls/components/call_message/call_message.tsx
@@ -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}
/>
-
+ >{'Call ended'}
-
- ),
- }}
- />
+ >{'Ended at '}
+ {
+
+ }
{'•'}
-
+ >{`Lasted ${moment.duration(post.props.end_at - post.props.start_at).humanize(false)}`}
@@ -162,12 +150,9 @@ const CallMessage = ({post, user, teammateNameDisplay, confirmToJoin, alreadyInT
style={style.joinCallIcon}
/>
-
+ >{`${displayUsername(user, teammateNameDisplay)} started a call`}
{alreadyInTheCall &&
- }
+ >{'Current call'}}
{!alreadyInTheCall &&
- }
+ >{'Join call'}}
);
diff --git a/app/products/calls/components/current_call/__snapshots__/current_call.test.js.snap b/app/products/calls/components/current_call/__snapshots__/current_call.test.js.snap
index 4b9951509..abb43fd6e 100644
--- a/app/products/calls/components/current_call/__snapshots__/current_call.test.js.snap
+++ b/app/products/calls/components/current_call/__snapshots__/current_call.test.js.snap
@@ -22,8 +22,8 @@ exports[`CurrentCall should match snapshot muted 1`] = `
}
>
-
+ No one is talking
-
+ ~Channel Name
-
+ No one is talking
-
+ ~Channel Name
{
});
const CurrentCall = (props: Props) => {
+ const [speaker, setSpeaker] = useState(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) => {
-
+ {speaker && `${displayUsername(speaker, props.teammateNameDisplay)} is talking`}
+ {!speaker && 'No one is talking'}
-
+ {`~${props.channel.display_name}`}
{
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}
/>
diff --git a/app/products/calls/components/join_call/__snapshots__/join_call.test.js.snap b/app/products/calls/components/join_call/__snapshots__/join_call.test.js.snap
index ffeab48d0..c41928764 100644
--- a/app/products/calls/components/join_call/__snapshots__/join_call.test.js.snap
+++ b/app/products/calls/components/join_call/__snapshots__/join_call.test.js.snap
@@ -62,9 +62,7 @@ exports[`JoinCall should match snapshot 1`] = `
+ >
+ Call participants
+
}
userIds={
Array [
diff --git a/app/products/calls/components/join_call/join_call.tsx b/app/products/calls/components/join_call/join_call.tsx
index bcdf10c76..4c6092c60 100644
--- a/app/products/calls/components/join_call/join_call.tsx
+++ b/app/products/calls/components/join_call/join_call.tsx
@@ -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={
-
+ {'Call participants'}
}
/>
diff --git a/app/products/calls/components/leave_and_join_alert.tsx b/app/products/calls/components/leave_and_join_alert.tsx
index 4c0de85c7..5c49a92aa 100644
--- a/app/products/calls/components/leave_and_join_alert.tsx
+++ b/app/products/calls/components/leave_and_join_alert.tsx
@@ -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',
},
diff --git a/app/products/calls/components/start_call/__snapshots__/start_call.test.js.snap b/app/products/calls/components/start_call/__snapshots__/start_call.test.js.snap
index 056244493..27bc4df6c 100644
--- a/app/products/calls/components/start_call/__snapshots__/start_call.test.js.snap
+++ b/app/products/calls/components/start_call/__snapshots__/start_call.test.js.snap
@@ -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",
diff --git a/app/products/calls/components/start_call/start_call.tsx b/app/products/calls/components/start_call/start_call.tsx
index 4b976f023..f84450310 100644
--- a/app/products/calls/components/start_call/start_call.tsx
+++ b/app/products/calls/components/start_call/start_call.tsx
@@ -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}
/>
diff --git a/app/products/calls/connection.ts b/app/products/calls/connection.ts
index a8b2a5c07..aa88b68e1 100644
--- a/app/products/calls/connection.ts
+++ b/app/products/calls/connection.ts
@@ -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);
diff --git a/app/products/calls/screens/call/__snapshots__/call_screen.test.js.snap b/app/products/calls/screens/call/__snapshots__/call_screen.test.js.snap
index b0ffaa522..08ceff413 100644
--- a/app/products/calls/screens/call/__snapshots__/call_screen.test.js.snap
+++ b/app/products/calls/screens/call/__snapshots__/call_screen.test.js.snap
@@ -69,7 +69,7 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
-
+ >
+ Leave
+
-
+ >
+ Chat thread
+
-
+ >
+ Settings
+
-
+ >
+ More
+
-
+ >
+ Unmute
+
@@ -445,21 +445,16 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
}
}
/>
-
+ >
+ You are seeing User 2 screen
+
-
+ >
+ Leave
+
-
+ >
+ Chat thread
+
-
+ >
+ Settings
+
-
+ >
+ More
+
-
+ >
+ Unmute
+
@@ -736,7 +731,7 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
-
+ >
+ Unmute
+
-
+ >
+ Leave
+
-
+ >
+ Chat thread
+
-
+ >
+ Settings
+
-
+ >
+ More
+
@@ -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}
/>
-
+ >
+ You are seeing User 2 screen
+
-
+ >
+ Unmute
+
-
+ >
+ Leave
+
-
+ >
+ Chat thread
+
-
+ >
+ Settings
+
-
+ >
+ More
+
@@ -1495,21 +1485,16 @@ exports[`CallScreen should show controls in landscape view on click the screen s
}
}
/>
-
+ >
+ You are seeing User 2 screen
+
-
+ >
+ Leave
+
-
+ >
+ Chat thread
+
-
+ >
+ Settings
+
-
+ >
+ More
+
-
+ >
+ Unmute
+
@@ -1790,7 +1775,7 @@ exports[`CallScreen should show controls in landscape view on click the users li
-
+ >
+ Leave
+
-
+ >
+ Chat thread
+
-
+ >
+ Settings
+
-
+ >
+ More
+
-
+ >
+ Unmute
+
diff --git a/app/products/calls/screens/call/call_screen.test.js b/app/products/calls/screens/call/call_screen.test.js
index b2dbda5b2..f28578577 100644
--- a/app/products/calls/screens/call/call_screen.test.js
+++ b/app/products/calls/screens/call/call_screen.test.js
@@ -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();
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();
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();
expect(wrapper.getElement()).toMatchSnapshot();
diff --git a/app/products/calls/screens/call/call_screen.tsx b/app/products/calls/screens/call/call_screen.tsx
index ed5fccdcb..c8ee7eeb4 100644
--- a/app/products/calls/screens/call/call_screen.tsx
+++ b/app/products/calls/screens/call/call_screen.tsx
@@ -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;
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(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}
/>
-
+ >{`You are seeing ${displayUsername(props.call.participants[props.call.screenOn].profile, props.teammateNameDisplay)} screen`}
);
}
@@ -307,11 +324,11 @@ const CallScreen = (props: Props) => {
>
- {displayUsername(props.users[user.id], props.teammateNameDisplay)}
+ {displayUsername(props.call?.participants[user.id].profile, props.teammateNameDisplay)}
);
})}
@@ -354,17 +371,13 @@ const CallScreen = (props: Props) => {
style={style.muteIcon}
/>
{props.currentParticipant?.muted &&
- }
+ >{'Unmute'}}
{!props.currentParticipant?.muted &&
- }
+ >{'Mute'}}
}
{
size={24}
style={{...style.buttonIcon, ...style.hangUpIcon}}
/>
-
+ >{'Leave'}
{
size={24}
style={style.buttonIcon}
/>
-
+ >{'Chat thread'}
{
size={24}
style={style.buttonIcon}
/>
-
+ >{'Settings'}
{
size={24}
style={style.buttonIcon}
/>
-
+ >{'More'}
{isLandscape &&
{
style={{...style.buttonIcon, ...style.muteIconLandscape}}
/>
{props.currentParticipant?.muted &&
- }
+ >{'Unmute'}}
{!props.currentParticipant?.muted &&
- }
+ >{'Mute'}}
}
diff --git a/app/products/calls/screens/call/index.ts b/app/products/calls/screens/call/index.ts
index 2901119fc..ddaa767ac 100644
--- a/app/products/calls/screens/call/index.ts
+++ b/app/products/calls/screens/call/index.ts
@@ -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),
};
diff --git a/app/products/calls/screens/call_other_actions/call_other_actions.tsx b/app/products/calls/screens/call_other_actions/call_other_actions.tsx
index 0f79e7cd4..1b697a28b 100644
--- a/app/products/calls/screens/call_other_actions/call_other_actions.tsx
+++ b/app/products/calls/screens/call_other_actions/call_other_actions.tsx
@@ -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}
/>
diff --git a/app/products/calls/simple-peer.ts b/app/products/calls/simple-peer.ts
index 62270321b..11aa7a2de 100644
--- a/app/products/calls/simple-peer.ts
+++ b/app/products/calls/simple-peer.ts
@@ -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;
diff --git a/app/products/calls/store/actions/calls.ts b/app/products/calls/store/actions/calls.ts
index 0a595bb0f..b3736a1b0 100644
--- a/app/products/calls/store/actions/calls.ts
+++ b/app/products/calls/store/actions/calls.ts
@@ -31,8 +31,9 @@ export function loadCalls(): ActionFunc {
if (channel.call) {
callsResults[channel.channel_id] = {
participants: channel.call.users.reduce((prev: Dictionary, 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,
diff --git a/app/products/calls/store/actions/websockets.ts b/app/products/calls/store/actions/websockets.ts
index d798a58ae..972bc7846 100644
--- a/app/products/calls/store/actions/websockets.ts
+++ b/app/products/calls/store/actions/websockets.ts
@@ -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 {
diff --git a/app/products/calls/store/reducers/calls.test.js b/app/products/calls/store/reducers/calls.test.js
index b29fe203a..fb8de8541 100644
--- a/app/products/calls/store/reducers/calls.test.js
+++ b/app/products/calls/store/reducers/calls.test.js
@@ -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 = {
diff --git a/app/products/calls/store/reducers/calls.ts b/app/products/calls/store/reducers/calls.ts
index e568f016e..db72040c5 100644
--- a/app/products/calls/store/reducers/calls.ts
+++ b/app/products/calls/store/reducers/calls.ts
@@ -31,7 +31,7 @@ function calls(state: Dictionary = {}, 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 = {}, action: GenericAction) {
id: userId,
muted: true,
isTalking: false,
+ profile,
};
const nextState = {...state};
nextState[channelId] = channelUpdate;
@@ -87,41 +88,6 @@ function calls(state: Dictionary = {}, 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]) {
diff --git a/app/products/calls/store/types/calls.ts b/app/products/calls/store/types/calls.ts
index a81210a93..04bbeb152 100644
--- a/app/products/calls/store/types/calls.ts
+++ b/app/products/calls/store/types/calls.ts
@@ -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;
+}
diff --git a/app/products/calls/websocket.ts b/app/products/calls/websocket.ts
new file mode 100644
index 000000000..69536a5c4
--- /dev/null
+++ b/app/products/calls/websocket.ts
@@ -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;
+ }
+}
diff --git a/app/screens/channel_info/channel_info.js b/app/screens/channel_info/channel_info.js
index 0c1a97e23..34c13ac24 100644
--- a/app/screens/channel_info/channel_info.js
+++ b/app/screens/channel_info/channel_info.js
@@ -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}
/>
>}
diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js
index 9a1c04124..3f02bee0c 100644
--- a/app/screens/channel_info/index.js
+++ b/app/screens/channel_info/index.js
@@ -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,
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 5f2e66fdd..561994d48 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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",
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b0e7913e6..7a1f31efb 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -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
diff --git a/package-lock.json b/package-lock.json
index e9b2b4314..4dc3894de 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index ad7270454..b38d38b17 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/types/modules/react-native-webrtc2.d.ts b/types/modules/react-native-webrtc.d.ts
similarity index 98%
rename from types/modules/react-native-webrtc2.d.ts
rename to types/modules/react-native-webrtc.d.ts
index f5e71102d..60f3314e5 100644
--- a/types/modules/react-native-webrtc2.d.ts
+++ b/types/modules/react-native-webrtc.d.ts
@@ -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;