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:
parent
c06a24c1ce
commit
2267f0a408
36 changed files with 693 additions and 638 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
100
app/products/calls/websocket.ts
Normal file
100
app/products/calls/websocket.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
</>}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
93
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in a new issue