MM-47004 - Calls: Client-side errors for microphone permissions (#6669)
* call error bar for microphone permissions; global permissions state * i18n * refactor permissionErrorBar component, PR comments * add module dependency's mocks for tests * fix error bar height * change permissions error text * working on 46999 redo audio handling -- will revert * Revert "working on 46999 redo audio handling -- will revert" This reverts commit 87bafc452c6ad6e1d7ae79ce78a0f2b461c2f150. * only get voice track when we have mic permissions * Android: enable mic when permissions are granted
This commit is contained in:
parent
cc5331f2ba
commit
8374d7e87f
20 changed files with 533 additions and 157 deletions
|
|
@ -23,6 +23,7 @@ export const SEARCH_INPUT_MARGIN = 5;
|
|||
|
||||
export const JOIN_CALL_BAR_HEIGHT = 38;
|
||||
export const CURRENT_CALL_BAR_HEIGHT = 74;
|
||||
export const CALL_ERROR_BAR_HEIGHT = 62;
|
||||
|
||||
export const QUICK_OPTIONS_HEIGHT = 270;
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id');
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', true);
|
||||
userJoinedCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id');
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', true);
|
||||
userJoinedCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
assert.equal(response!.data, 'channel-id');
|
||||
|
|
@ -191,7 +191,7 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id');
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', true);
|
||||
userJoinedCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
assert.equal(response!.data, 'channel-id');
|
||||
|
|
@ -218,7 +218,7 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id');
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', true);
|
||||
userJoinedCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
assert.equal(response!.data, 'channel-id');
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ export const enableChannelCalls = async (serverUrl: string, channelId: string, e
|
|||
return {};
|
||||
};
|
||||
|
||||
export const joinCall = async (serverUrl: string, channelId: string): Promise<{ error?: string | Error; data?: string }> => {
|
||||
export const joinCall = async (serverUrl: string, channelId: string, hasMicPermission: boolean): Promise<{ error?: string | Error; data?: string }> => {
|
||||
// Edge case: calls was disabled when app loaded, and then enabled, but app hasn't
|
||||
// reconnected its websocket since then (i.e., hasn't called batchLoadCalls yet)
|
||||
const {data: enabled} = await checkIsCallsPluginEnabled(serverUrl);
|
||||
|
|
@ -233,7 +233,7 @@ export const joinCall = async (serverUrl: string, channelId: string): Promise<{
|
|||
setSpeakerphoneOn(false);
|
||||
|
||||
try {
|
||||
connection = await newConnection(serverUrl, channelId, () => null, setScreenShareURL);
|
||||
connection = await newConnection(serverUrl, channelId, () => null, setScreenShareURL, hasMicPermission);
|
||||
} catch (error: unknown) {
|
||||
await forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error: error as Error};
|
||||
|
|
@ -270,6 +270,12 @@ export const unmuteMyself = () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const initializeVoiceTrack = () => {
|
||||
if (connection) {
|
||||
connection.initializeVoiceTrack();
|
||||
}
|
||||
};
|
||||
|
||||
export const raiseHand = () => {
|
||||
if (connection) {
|
||||
connection.raiseHand();
|
||||
|
|
|
|||
|
|
@ -1,28 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Alert, Platform} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {Platform} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
const getMicrophonePermissionDeniedMessage = (intl: IntlShape) => {
|
||||
const {formatMessage} = intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.microphone_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your microphone',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.microphone_permission_denied_description',
|
||||
defaultMessage: 'To participate in this call, open Settings to grant Mattermost access to your microphone.',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const hasMicrophonePermission = async (intl: IntlShape) => {
|
||||
export const hasMicrophonePermission = async () => {
|
||||
const targetSource = Platform.select({
|
||||
ios: Permissions.PERMISSIONS.IOS.MICROPHONE,
|
||||
default: Permissions.PERMISSIONS.ANDROID.RECORD_AUDIO,
|
||||
|
|
@ -36,32 +18,8 @@ export const hasMicrophonePermission = async (intl: IntlShape) => {
|
|||
|
||||
return permissionRequest === Permissions.RESULTS.GRANTED;
|
||||
}
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const grantOption = {
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
|
||||
const {title, text} = getMicrophonePermissionDeniedMessage(intl);
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
case Permissions.RESULTS.BLOCKED:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import {Alert} from 'react-native';
|
||||
|
||||
import {hasMicrophonePermission, joinCall, unmuteMyself} from '@calls/actions';
|
||||
import {setMicPermissionsGranted} from '@calls/state';
|
||||
import {errorAlert} from '@calls/utils';
|
||||
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
|
@ -89,16 +90,10 @@ export const leaveAndJoinWithAlert = (
|
|||
const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolean, intl: IntlShape) => {
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const hasPermission = await hasMicrophonePermission(intl);
|
||||
if (!hasPermission) {
|
||||
errorAlert(formatMessage({
|
||||
id: 'mobile.calls_error_permissions',
|
||||
defaultMessage: 'No permissions to microphone, unable to start call',
|
||||
}), intl);
|
||||
return;
|
||||
}
|
||||
const hasPermission = await hasMicrophonePermission();
|
||||
setMicPermissionsGranted(hasPermission);
|
||||
|
||||
const res = await joinCall(serverUrl, channelId);
|
||||
const res = await joinCall(serverUrl, channelId, hasPermission);
|
||||
if (res.error) {
|
||||
const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'See server logs'});
|
||||
errorAlert(res.error?.toString() || seeLogs, intl);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import {Options} from 'react-native-navigation';
|
|||
|
||||
import {muteMyself, unmuteMyself} from '@calls/actions';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
import {usePermissionsChecker} from '@calls/hooks';
|
||||
import {CurrentCall} from '@calls/types/calls';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
|
|
@ -24,6 +27,7 @@ type Props = {
|
|||
currentCall: CurrentCall | null;
|
||||
userModelsDict: Dictionary<UserModel>;
|
||||
teammateNameDisplay: string;
|
||||
micPermissionsGranted: boolean;
|
||||
threadScreen?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -57,18 +61,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
color: theme.sidebarText,
|
||||
opacity: 0.64,
|
||||
},
|
||||
micIcon: {
|
||||
color: theme.sidebarText,
|
||||
micIconContainer: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#3DB887',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
borderRadius: 4,
|
||||
margin: 4,
|
||||
padding: 9,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
micIcon: {
|
||||
color: theme.sidebarText,
|
||||
},
|
||||
muted: {
|
||||
backgroundColor: 'transparent',
|
||||
|
|
@ -86,10 +90,13 @@ const CurrentCallBar = ({
|
|||
currentCall,
|
||||
userModelsDict,
|
||||
teammateNameDisplay,
|
||||
micPermissionsGranted,
|
||||
threadScreen,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const {formatMessage} = useIntl();
|
||||
usePermissionsChecker(micPermissionsGranted);
|
||||
|
||||
const goToCallScreen = useCallback(async () => {
|
||||
const options: Options = {
|
||||
|
|
@ -134,42 +141,48 @@ const CurrentCallBar = ({
|
|||
}
|
||||
};
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
|
||||
|
||||
return (
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.container}>
|
||||
<CallAvatar
|
||||
userModel={userModelsDict[speaker || '']}
|
||||
volume={speaker ? 0.5 : 0}
|
||||
serverUrl={currentCall?.serverUrl || ''}
|
||||
/>
|
||||
<View style={style.userInfo}>
|
||||
<Text style={style.speakingUser}>{talkingMessage}</Text>
|
||||
<Text style={style.currentChannel}>{`~${displayName}`}</Text>
|
||||
<>
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.container}>
|
||||
<CallAvatar
|
||||
userModel={userModelsDict[speaker || '']}
|
||||
volume={speaker ? 0.5 : 0}
|
||||
serverUrl={currentCall?.serverUrl || ''}
|
||||
/>
|
||||
<View style={style.userInfo}>
|
||||
<Text style={style.speakingUser}>{talkingMessage}</Text>
|
||||
<Text style={style.currentChannel}>{`~${displayName}`}</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPressIn={goToCallScreen}
|
||||
style={style.pressable}
|
||||
>
|
||||
<CompassIcon
|
||||
name='arrow-expand'
|
||||
size={24}
|
||||
style={style.expandIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
<TouchableOpacity
|
||||
onPress={muteUnmute}
|
||||
style={[style.pressable, style.micIconContainer, myParticipant?.muted && style.muted]}
|
||||
disabled={!micPermissionsGranted}
|
||||
>
|
||||
<UnavailableIconWrapper
|
||||
name={myParticipant?.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
unavailable={!micPermissionsGranted}
|
||||
style={[style.micIcon]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Pressable
|
||||
onPressIn={goToCallScreen}
|
||||
style={style.pressable}
|
||||
>
|
||||
<CompassIcon
|
||||
name='arrow-expand'
|
||||
size={24}
|
||||
style={style.expandIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
<TouchableOpacity
|
||||
onPress={muteUnmute}
|
||||
style={style.pressable}
|
||||
>
|
||||
<CompassIcon
|
||||
name={myParticipant?.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
style={[style.micIcon, myParticipant?.muted ? style.muted : undefined]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentCallBar;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import withObservables from '@nozbe/with-observables';
|
|||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeCurrentCall} from '@calls/state';
|
||||
import {observeCurrentCall, observeGlobalCallsState} from '@calls/state';
|
||||
import {idsAreEqual} from '@calls/utils';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
|
|
@ -45,12 +45,17 @@ const enhanced = withObservables([], () => {
|
|||
const teammateNameDisplay = database.pipe(
|
||||
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
|
||||
);
|
||||
const micPermissionsGranted = observeGlobalCallsState().pipe(
|
||||
switchMap((gs) => of$(gs.micPermissionsGranted)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
return {
|
||||
displayName,
|
||||
currentCall,
|
||||
userModelsDict,
|
||||
teammateNameDisplay,
|
||||
micPermissionsGranted,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
104
app/products/calls/components/permission_error_bar.tsx
Normal file
104
app/products/calls/components/permission_error_bar.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Pressable, View} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {setMicPermissionsErrorDismissed} from '@calls/state';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {CALL_ERROR_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => (
|
||||
{
|
||||
pressable: {
|
||||
zIndex: 10,
|
||||
},
|
||||
errorWrapper: {
|
||||
padding: 10,
|
||||
paddingTop: 0,
|
||||
},
|
||||
errorBar: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: theme.dndIndicator,
|
||||
minHeight: CALL_ERROR_BAR_HEIGHT,
|
||||
width: '100%',
|
||||
borderRadius: 5,
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
errorText: {
|
||||
flex: 1,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
color: theme.buttonColor,
|
||||
},
|
||||
errorIconContainer: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
margin: 0,
|
||||
padding: 9,
|
||||
},
|
||||
pressedErrorIconContainer: {
|
||||
backgroundColor: theme.buttonColor,
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 18,
|
||||
},
|
||||
pressedErrorIcon: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
paddingRight: {
|
||||
paddingRight: 9,
|
||||
},
|
||||
}
|
||||
));
|
||||
|
||||
const PermissionErrorBar = () => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.errorWrapper}>
|
||||
<Pressable
|
||||
onPress={Permissions.openSettings}
|
||||
style={style.errorBar}
|
||||
>
|
||||
<CompassIcon
|
||||
name='microphone-off'
|
||||
style={[style.errorIcon, style.paddingRight]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_mic_error'}
|
||||
defaultMessage={'To participate, open Settings to grant Mattermost access to your microphone.'}
|
||||
style={style.errorText}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={setMicPermissionsErrorDismissed}
|
||||
hitSlop={5}
|
||||
style={({pressed}) => [
|
||||
style.pressable,
|
||||
style.errorIconContainer,
|
||||
pressed && style.pressedErrorIconContainer,
|
||||
]}
|
||||
>
|
||||
{({pressed}) => (
|
||||
<CompassIcon
|
||||
name='close'
|
||||
style={[style.errorIcon, pressed && style.pressedErrorIcon]}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionErrorBar;
|
||||
68
app/products/calls/components/unavailable_icon_wrapper.tsx
Normal file
68
app/products/calls/components/unavailable_icon_wrapper.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, TextStyle, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
size: number;
|
||||
style: StyleProp<TextStyle>;
|
||||
unavailable: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
position: 'relative',
|
||||
},
|
||||
unavailable: {
|
||||
color: changeOpacity(theme.sidebarText, 0.32),
|
||||
},
|
||||
errorContainer: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
backgroundColor: '#3F4350',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#3F4350',
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.dndIndicator,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const UnavailableIconWrapper = ({name, size, style: providedStyle, unavailable}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const errorIconSize = size / 2;
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<CompassIcon
|
||||
name={name}
|
||||
size={size}
|
||||
style={[providedStyle, unavailable && style.unavailable]}
|
||||
/>
|
||||
{unavailable &&
|
||||
<View
|
||||
style={[style.errorContainer, {borderRadius: errorIconSize / 2}]}
|
||||
>
|
||||
<CompassIcon
|
||||
name={'close-circle'}
|
||||
size={errorIconSize}
|
||||
style={style.errorIcon}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnavailableIconWrapper;
|
||||
|
|
@ -25,7 +25,13 @@ import type {CallsConnection} from '@calls/types/calls';
|
|||
|
||||
const peerConnectTimeout = 5000;
|
||||
|
||||
export async function newConnection(serverUrl: string, channelID: string, closeCb: () => void, setScreenShareURL: (url: string) => void) {
|
||||
export async function newConnection(
|
||||
serverUrl: string,
|
||||
channelID: string,
|
||||
closeCb: () => void,
|
||||
setScreenShareURL: (url: string) => void,
|
||||
hasMicPermission: boolean,
|
||||
) {
|
||||
let peer: Peer | null = null;
|
||||
let stream: MediaStream;
|
||||
let voiceTrackAdded = false;
|
||||
|
|
@ -34,17 +40,23 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
|
|||
let onCallEnd: EmitterSubscription | null = null;
|
||||
const streams: MediaStream[] = [];
|
||||
|
||||
try {
|
||||
stream = await mediaDevices.getUserMedia({
|
||||
video: false,
|
||||
audio: true,
|
||||
}) as MediaStream;
|
||||
voiceTrack = stream.getAudioTracks()[0];
|
||||
voiceTrack.enabled = false;
|
||||
streams.push(stream);
|
||||
} catch (err) {
|
||||
logError('Unable to get media device:', err);
|
||||
}
|
||||
const initializeVoiceTrack = async () => {
|
||||
if (voiceTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
stream = await mediaDevices.getUserMedia({
|
||||
video: false,
|
||||
audio: true,
|
||||
}) as MediaStream;
|
||||
voiceTrack = stream.getAudioTracks()[0];
|
||||
voiceTrack.enabled = false;
|
||||
streams.push(stream);
|
||||
} catch (err) {
|
||||
logError('Unable to get media device:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// getClient can throw an error, which will be handled by the caller.
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
|
@ -56,6 +68,10 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
|
|||
// Throws an error, to be caught by caller.
|
||||
await ws.initialize();
|
||||
|
||||
if (hasMicPermission) {
|
||||
initializeVoiceTrack();
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
if (isClosed) {
|
||||
return;
|
||||
|
|
@ -265,6 +281,7 @@ export async function newConnection(serverUrl: string, channelID: string, closeC
|
|||
waitForPeerConnection,
|
||||
raiseHand,
|
||||
unraiseHand,
|
||||
initializeVoiceTrack,
|
||||
};
|
||||
|
||||
return connection;
|
||||
|
|
|
|||
|
|
@ -3,14 +3,18 @@
|
|||
|
||||
// Check if calls is enabled. If it is, then run fn; if it isn't, show an alert and set
|
||||
// msgPostfix to ' (Not Available)'.
|
||||
import {useCallback, useState} from 'react';
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
import {Alert, Platform} from 'react-native';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {initializeVoiceTrack} from '@calls/actions/calls';
|
||||
import {setMicPermissionsGranted} from '@calls/state';
|
||||
import {errorAlert} from '@calls/utils';
|
||||
import {Client} from '@client/rest';
|
||||
import ClientError from '@client/rest/error';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useAppState} from '@hooks/device';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
export const useTryCallsFunction = (fn: () => void) => {
|
||||
|
|
@ -71,3 +75,27 @@ export const useTryCallsFunction = (fn: () => void) => {
|
|||
|
||||
return [tryFn, msgPostfix] as [() => Promise<void>, string];
|
||||
};
|
||||
|
||||
const micPermission = Platform.select({
|
||||
ios: Permissions.PERMISSIONS.IOS.MICROPHONE,
|
||||
default: Permissions.PERMISSIONS.ANDROID.RECORD_AUDIO,
|
||||
});
|
||||
|
||||
export const usePermissionsChecker = (micPermissionsGranted: boolean) => {
|
||||
const appState = useAppState();
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFn = async () => {
|
||||
if (appState === 'active') {
|
||||
const hasPermission = (await Permissions.check(micPermission)) === Permissions.RESULTS.GRANTED;
|
||||
if (hasPermission) {
|
||||
initializeVoiceTrack();
|
||||
setMicPermissionsGranted(hasPermission);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!micPermissionsGranted) {
|
||||
asyncFn();
|
||||
}
|
||||
}, [appState]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ import {
|
|||
} from '@calls/actions';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import CallDuration from '@calls/components/call_duration';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
import {usePermissionsChecker} from '@calls/hooks';
|
||||
import RaisedHandIcon from '@calls/icons/raised_hand_icon';
|
||||
import UnraisedHandIcon from '@calls/icons/unraised_hand_icon';
|
||||
import {CallParticipant, CurrentCall} from '@calls/types/calls';
|
||||
|
|
@ -48,13 +51,14 @@ import {
|
|||
import NavigationStore from '@store/navigation_store';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {mergeNavigationOptions} from '@utils/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
export type Props = {
|
||||
componentId: string;
|
||||
currentCall: CurrentCall | null;
|
||||
participantsDict: Dictionary<CallParticipant>;
|
||||
micPermissionsGranted: boolean;
|
||||
teammateNameDisplay: string;
|
||||
fromThreadScreen?: boolean;
|
||||
}
|
||||
|
|
@ -252,19 +256,31 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
color: 'white',
|
||||
margin: 3,
|
||||
},
|
||||
unavailableText: {
|
||||
color: changeOpacity(theme.sidebarText, 0.32),
|
||||
},
|
||||
}));
|
||||
|
||||
const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDisplay, fromThreadScreen}: Props) => {
|
||||
const CallScreen = ({
|
||||
componentId,
|
||||
currentCall,
|
||||
participantsDict,
|
||||
micPermissionsGranted,
|
||||
teammateNameDisplay,
|
||||
fromThreadScreen,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const {width, height} = useWindowDimensions();
|
||||
usePermissionsChecker(micPermissionsGranted);
|
||||
const [showControlsInLandscape, setShowControlsInLandscape] = useState(false);
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const isLandscape = width > height;
|
||||
const showControls = !isLandscape || showControlsInLandscape;
|
||||
const myParticipant = currentCall?.participants[currentCall.myUserId];
|
||||
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
|
||||
const chatThreadTitle = intl.formatMessage({id: 'mobile.calls_chat_thread', defaultMessage: 'Chat thread'});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -463,7 +479,7 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
<FormattedText
|
||||
id={'mobile.calls_unmute'}
|
||||
defaultMessage={'Unmute'}
|
||||
style={style.buttonText}
|
||||
style={[style.buttonText, !micPermissionsGranted && style.unavailableText]}
|
||||
/>);
|
||||
|
||||
return (
|
||||
|
|
@ -487,6 +503,7 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
</View>
|
||||
{usersList}
|
||||
{screenShareView}
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
<View
|
||||
style={[style.buttons, isLandscape && style.buttonsLandscape, !showControls && style.buttonsLandscapeNoControls]}
|
||||
>
|
||||
|
|
@ -495,10 +512,12 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
|||
testID='mute-unmute'
|
||||
style={[style.mute, myParticipant.muted && style.muteMuted]}
|
||||
onPress={muteUnmuteHandler}
|
||||
disabled={!micPermissionsGranted}
|
||||
>
|
||||
<CompassIcon
|
||||
<UnavailableIconWrapper
|
||||
name={myParticipant.muted ? 'microphone-off' : 'microphone'}
|
||||
size={24}
|
||||
unavailable={!micPermissionsGranted}
|
||||
style={style.muteIcon}
|
||||
/>
|
||||
{myParticipant.muted ? UnmuteText : MuteText}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {combineLatest, of as of$} from 'rxjs';
|
|||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import CallScreen from '@calls/screens/call_screen/call_screen';
|
||||
import {observeCurrentCall} from '@calls/state';
|
||||
import {observeCurrentCall, observeGlobalCallsState} from '@calls/state';
|
||||
import {CallParticipant} from '@calls/types/calls';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {observeTeammateNameDisplay, queryUsersById} from '@queries/servers/user';
|
||||
|
|
@ -34,6 +34,10 @@ const enhanced = withObservables([], () => {
|
|||
}, {} as Dictionary<CallParticipant>))),
|
||||
)),
|
||||
);
|
||||
const micPermissionsGranted = observeGlobalCallsState().pipe(
|
||||
switchMap((gs) => of$(gs.micPermissionsGranted)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const teammateNameDisplay = database.pipe(
|
||||
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
|
||||
distinctUntilChanged(),
|
||||
|
|
@ -42,6 +46,7 @@ const enhanced = withObservables([], () => {
|
|||
return {
|
||||
currentCall,
|
||||
participantsDict,
|
||||
micPermissionsGranted,
|
||||
teammateNameDisplay,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ import {
|
|||
setCallsState,
|
||||
setChannelsWithCalls,
|
||||
setCurrentCall,
|
||||
setMicPermissionsErrorDismissed,
|
||||
setMicPermissionsGranted,
|
||||
useCallsConfig,
|
||||
useCallsState,
|
||||
useChannelsWithCalls,
|
||||
useCurrentCall,
|
||||
useCurrentCall, useGlobalCallsState,
|
||||
} from '@calls/state';
|
||||
import {
|
||||
setCalls,
|
||||
|
|
@ -34,9 +36,18 @@ import {
|
|||
} from '@calls/state/actions';
|
||||
import {License} from '@constants';
|
||||
|
||||
import {CallsState, CurrentCall, DefaultCallsConfig, DefaultCallsState} from '../types/calls';
|
||||
import {
|
||||
Call,
|
||||
CallsState,
|
||||
CurrentCall,
|
||||
DefaultCallsConfig,
|
||||
DefaultCallsState,
|
||||
DefaultCurrentCall,
|
||||
DefaultGlobalCallsState,
|
||||
GlobalCallsState,
|
||||
} from '../types/calls';
|
||||
|
||||
const call1 = {
|
||||
const call1: Call = {
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
|
||||
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
|
||||
|
|
@ -47,7 +58,7 @@ const call1 = {
|
|||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
};
|
||||
const call2 = {
|
||||
const call2: Call = {
|
||||
participants: {
|
||||
'user-3': {id: 'user-3', muted: false, raisedHand: 0},
|
||||
'user-4': {id: 'user-4', muted: true, raisedHand: 0},
|
||||
|
|
@ -58,7 +69,7 @@ const call2 = {
|
|||
threadId: 'thread-2',
|
||||
ownerId: 'user-3',
|
||||
};
|
||||
const call3 = {
|
||||
const call3: Call = {
|
||||
participants: {
|
||||
'user-5': {id: 'user-5', muted: false, raisedHand: 0},
|
||||
'user-6': {id: 'user-6', muted: true, raisedHand: 0},
|
||||
|
|
@ -109,12 +120,10 @@ describe('useCallsState', () => {
|
|||
'channel-1': true,
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
};
|
||||
const testNewCall1 = {
|
||||
...call1,
|
||||
|
|
@ -181,13 +190,12 @@ describe('useCallsState', () => {
|
|||
const initialChannelsWithCallsState = {
|
||||
'channel-1': true,
|
||||
};
|
||||
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
};
|
||||
const expectedCallsState = {
|
||||
'channel-1': {
|
||||
|
|
@ -242,12 +250,10 @@ describe('useCallsState', () => {
|
|||
'channel-1': true,
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
};
|
||||
const expectedCallsState = {
|
||||
'channel-1': {
|
||||
|
|
@ -345,12 +351,10 @@ describe('useCallsState', () => {
|
|||
};
|
||||
const initialChannelsWithCallsState = {'channel-1': true, 'channel-2': true};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
};
|
||||
|
||||
// setup
|
||||
|
|
@ -393,12 +397,10 @@ describe('useCallsState', () => {
|
|||
};
|
||||
const initialChannelsWithCallsState = {'channel-1': true, 'channel-2': true};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
};
|
||||
|
||||
// setup
|
||||
|
|
@ -452,12 +454,10 @@ describe('useCallsState', () => {
|
|||
},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
};
|
||||
const expectedCurrentCallState = {
|
||||
...initialCurrentCallState,
|
||||
|
|
@ -511,12 +511,10 @@ describe('useCallsState', () => {
|
|||
},
|
||||
};
|
||||
const expectedCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
...newCall1,
|
||||
voiceOn: {},
|
||||
};
|
||||
|
||||
// setup
|
||||
|
|
@ -657,6 +655,79 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[1], null);
|
||||
});
|
||||
|
||||
it('MicPermissions', () => {
|
||||
const initialGlobalState = DefaultGlobalCallsState;
|
||||
const initialCallsState: CallsState = {
|
||||
...DefaultCallsState,
|
||||
myUserId: 'myUserId',
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const newCall1: Call = {
|
||||
...call1,
|
||||
participants: {
|
||||
...call1.participants,
|
||||
myUserId: {id: 'myUserId', muted: true, raisedHand: 0},
|
||||
},
|
||||
};
|
||||
const expectedCallsState: CallsState = {
|
||||
...initialCallsState,
|
||||
calls: {
|
||||
...initialCallsState.calls,
|
||||
'channel-1': newCall1,
|
||||
},
|
||||
};
|
||||
const expectedCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...newCall1,
|
||||
};
|
||||
const secondExpectedCurrentCallState: CurrentCall = {
|
||||
...expectedCurrentCallState,
|
||||
micPermissionsErrorDismissed: true,
|
||||
};
|
||||
const expectedGlobalState: GlobalCallsState = {
|
||||
micPermissionsGranted: true,
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCurrentCall(), useGlobalCallsState()];
|
||||
});
|
||||
act(() => setCallsState('server1', initialCallsState));
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], null);
|
||||
assert.deepEqual(result.current[2], initialGlobalState);
|
||||
|
||||
// join call
|
||||
act(() => {
|
||||
setMicPermissionsGranted(false);
|
||||
userJoinedCall('server1', 'channel-1', 'myUserId');
|
||||
});
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], expectedCurrentCallState);
|
||||
assert.deepEqual(result.current[2], initialGlobalState);
|
||||
|
||||
// dismiss mic error
|
||||
act(() => setMicPermissionsErrorDismissed());
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], secondExpectedCurrentCallState);
|
||||
assert.deepEqual(result.current[2], initialGlobalState);
|
||||
|
||||
// grant permissions
|
||||
act(() => setMicPermissionsGranted(true));
|
||||
assert.deepEqual(result.current[0], expectedCallsState);
|
||||
assert.deepEqual(result.current[1], secondExpectedCurrentCallState);
|
||||
assert.deepEqual(result.current[2], expectedGlobalState);
|
||||
|
||||
act(() => {
|
||||
myselfLeftCall();
|
||||
userLeftCall('server1', 'channel-1', 'myUserId');
|
||||
});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], null);
|
||||
});
|
||||
|
||||
it('voiceOn and Off', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
|
|
@ -665,12 +736,10 @@ describe('useCallsState', () => {
|
|||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
};
|
||||
|
||||
// setup
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import {
|
|||
getCallsState,
|
||||
getChannelsWithCalls,
|
||||
getCurrentCall,
|
||||
getGlobalCallsState,
|
||||
setCallsConfig,
|
||||
setCallsState,
|
||||
setChannelsWithCalls,
|
||||
setCurrentCall,
|
||||
setGlobalCallsState,
|
||||
} from '@calls/state';
|
||||
import {Call, CallsConfig, ChannelsWithCalls} from '@calls/types/calls';
|
||||
|
||||
|
|
@ -116,6 +118,7 @@ export const userJoinedCall = (serverUrl: string, channelId: string, userId: str
|
|||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
micPermissionsErrorDismissed: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -363,3 +366,26 @@ export const setPluginEnabled = (serverUrl: string, pluginEnabled: boolean) => {
|
|||
const callsConfig = getCallsConfig(serverUrl);
|
||||
setCallsConfig(serverUrl, {...callsConfig, pluginEnabled});
|
||||
};
|
||||
|
||||
export const setMicPermissionsGranted = (granted: boolean) => {
|
||||
const globalState = getGlobalCallsState();
|
||||
|
||||
const nextGlobalState = {
|
||||
...globalState,
|
||||
micPermissionsGranted: granted,
|
||||
};
|
||||
setGlobalCallsState(nextGlobalState);
|
||||
};
|
||||
|
||||
export const setMicPermissionsErrorDismissed = () => {
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
micPermissionsErrorDismissed: true,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
|
|
|||
38
app/products/calls/state/global_calls_state.ts
Normal file
38
app/products/calls/state/global_calls_state.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {DefaultGlobalCallsState, GlobalCallsState} from '@calls/types/calls';
|
||||
|
||||
const globalStateSubject = new BehaviorSubject(DefaultGlobalCallsState);
|
||||
|
||||
export const getGlobalCallsState = () => {
|
||||
return globalStateSubject.value;
|
||||
};
|
||||
|
||||
export const setGlobalCallsState = (globalState: GlobalCallsState) => {
|
||||
globalStateSubject.next(globalState);
|
||||
};
|
||||
|
||||
export const observeGlobalCallsState = () => {
|
||||
return globalStateSubject.asObservable();
|
||||
};
|
||||
|
||||
export const useGlobalCallsState = () => {
|
||||
const [state, setState] = useState<GlobalCallsState>(DefaultGlobalCallsState);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = globalStateSubject.subscribe((globalState) => {
|
||||
setState(globalState);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
|
|
@ -6,3 +6,4 @@ export * from './calls_state';
|
|||
export * from './calls_config';
|
||||
export * from './current_call';
|
||||
export * from './channels_with_calls';
|
||||
export * from './global_calls_state';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@
|
|||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {ConfigurationParamWithUrls, ConfigurationParamWithUrl} from 'react-native-webrtc';
|
||||
|
||||
export type GlobalCallsState = {
|
||||
micPermissionsGranted: boolean;
|
||||
}
|
||||
|
||||
export const DefaultGlobalCallsState: GlobalCallsState = {
|
||||
micPermissionsGranted: false,
|
||||
};
|
||||
|
||||
export type CallsState = {
|
||||
serverUrl: string;
|
||||
myUserId: string;
|
||||
|
|
@ -11,12 +19,12 @@ export type CallsState = {
|
|||
enabled: Dictionary<boolean>;
|
||||
}
|
||||
|
||||
export const DefaultCallsState = {
|
||||
export const DefaultCallsState: CallsState = {
|
||||
serverUrl: '',
|
||||
myUserId: '',
|
||||
calls: {} as Dictionary<Call>,
|
||||
enabled: {} as Dictionary<boolean>,
|
||||
} as CallsState;
|
||||
};
|
||||
|
||||
export type Call = {
|
||||
participants: Dictionary<CallParticipant>;
|
||||
|
|
@ -46,8 +54,23 @@ export type CurrentCall = {
|
|||
screenShareURL: string;
|
||||
speakerphoneOn: boolean;
|
||||
voiceOn: Dictionary<boolean>;
|
||||
micPermissionsErrorDismissed: boolean;
|
||||
}
|
||||
|
||||
export const DefaultCurrentCall: CurrentCall = {
|
||||
serverUrl: '',
|
||||
myUserId: '',
|
||||
participants: {},
|
||||
channelId: '',
|
||||
startTime: 0,
|
||||
screenOn: '',
|
||||
threadId: '',
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
micPermissionsErrorDismissed: false,
|
||||
};
|
||||
|
||||
export type CallParticipant = {
|
||||
id: string;
|
||||
muted: boolean;
|
||||
|
|
@ -90,6 +113,7 @@ export type CallsConnection = {
|
|||
waitForPeerConnection: () => Promise<void>;
|
||||
raiseHand: () => void;
|
||||
unraiseHand: () => void;
|
||||
initializeVoiceTrack: () => void;
|
||||
}
|
||||
|
||||
export type ServerCallsConfig = {
|
||||
|
|
@ -107,7 +131,7 @@ export type CallsConfig = ServerCallsConfig & {
|
|||
last_retrieved_at: number;
|
||||
}
|
||||
|
||||
export const DefaultCallsConfig = {
|
||||
export const DefaultCallsConfig: CallsConfig = {
|
||||
pluginEnabled: false,
|
||||
ICEServers: [], // deprecated
|
||||
ICEServersConfigs: [],
|
||||
|
|
@ -117,7 +141,7 @@ export const DefaultCallsConfig = {
|
|||
last_retrieved_at: 0,
|
||||
sku_short_name: '',
|
||||
MaxCallParticipants: 0,
|
||||
} as CallsConfig;
|
||||
};
|
||||
|
||||
export type ICEServersConfigs = Array<ConfigurationParamWithUrls | ConfigurationParamWithUrl>;
|
||||
|
||||
|
|
|
|||
|
|
@ -368,7 +368,6 @@
|
|||
"mobile.calls_end_permission_title": "Error",
|
||||
"mobile.calls_ended_at": "Ended at",
|
||||
"mobile.calls_error_message": "Error: {error}",
|
||||
"mobile.calls_error_permissions": "No permissions to microphone, unable to start call",
|
||||
"mobile.calls_error_title": "Error",
|
||||
"mobile.calls_join_call": "Join call",
|
||||
"mobile.calls_lasted": "Lasted {duration}",
|
||||
|
|
@ -377,6 +376,7 @@
|
|||
"mobile.calls_limit_msg": "The maximum number of participants per call is {maxParticipants}. Contact your System Admin to increase the limit.",
|
||||
"mobile.calls_limit_reached": "Participant limit reached",
|
||||
"mobile.calls_lower_hand": "Lower hand",
|
||||
"mobile.calls_mic_error": "To participate, open Settings to grant Mattermost access to your microphone.",
|
||||
"mobile.calls_more": "More",
|
||||
"mobile.calls_mute": "Mute",
|
||||
"mobile.calls_name_is_talking": "{name} is talking",
|
||||
|
|
@ -482,8 +482,6 @@
|
|||
"mobile.message_length.message": "Your current message is too long. Current character count: {count}/{max}",
|
||||
"mobile.message_length.message_split_left": "Message exceeds the character limit",
|
||||
"mobile.message_length.title": "Message Length",
|
||||
"mobile.microphone_permission_denied_description": "To participate in this call, open Settings to grant Mattermost access to your microphone.",
|
||||
"mobile.microphone_permission_denied_title": "{applicationName} would like to access your microphone",
|
||||
"mobile.no_results_with_term": "No results for “{term}”",
|
||||
"mobile.no_results_with_term.files": "No files matching “{term}”",
|
||||
"mobile.no_results_with_term.messages": "No matches found for “{term}”",
|
||||
|
|
@ -548,12 +546,9 @@
|
|||
"mobile.screen.settings": "Settings",
|
||||
"mobile.screen.your_profile": "Your Profile",
|
||||
"mobile.search.jump": "Jump to recent messages",
|
||||
"mobile.search.modifier.after": "after a date",
|
||||
"mobile.search.modifier.before": "before a date",
|
||||
"mobile.search.modifier.exclude": "exclude search terms",
|
||||
"mobile.search.modifier.from": "a specific user",
|
||||
"mobile.search.modifier.in": "a specific channel",
|
||||
"mobile.search.modifier.on": "a specific date",
|
||||
"mobile.search.modifier.phrases": "messages with phrases",
|
||||
"mobile.search.show_less": "Show less",
|
||||
"mobile.search.show_more": "Show more",
|
||||
|
|
|
|||
|
|
@ -128,6 +128,10 @@ jest.doMock('react-native', () => {
|
|||
},
|
||||
}),
|
||||
},
|
||||
WebRTCModule: {
|
||||
senderGetCapabilities: jest.fn().mockReturnValue(null),
|
||||
receiverGetCapabilities: jest.fn().mockReturnValue(null),
|
||||
},
|
||||
};
|
||||
|
||||
const Linking = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue