MM-51776 - Calls: Audio device selections, & fix bluetooth (#7286)

* ask for bluetooth permissions on Android

* new "switch audio device button" for Android

* i18n strings

* split audio_device_button into platform-specific files; PR comments

* earpiece -> phone

* add comments to clarify speakerphone logic

* update snapshots

* add new compass-icons; use tablet and bluetooth icons; add tablet mode

* fix lint
This commit is contained in:
Christopher Poile 2023-04-27 11:44:14 -04:00 committed by GitHub
parent 30706382a0
commit c1430757b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 368 additions and 70 deletions

View file

@ -43,6 +43,7 @@ import {newConnection} from '../connection/connection';
import type {
ApiResp,
AudioDevice,
Call,
CallParticipant,
CallsConnection,
@ -352,6 +353,10 @@ export const setSpeakerphoneOn = (speakerphoneOn: boolean) => {
setSpeakerPhone(speakerphoneOn);
};
export const setPreferredAudioRoute = async (audio: AudioDevice) => {
return InCallManager.chooseAudioRoute(audio);
};
export const canEndCall = async (serverUrl: string, channelId: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {

View file

@ -4,24 +4,45 @@
import {Platform} from 'react-native';
import Permissions from 'react-native-permissions';
export const hasMicrophonePermission = async () => {
const targetSource = Platform.select({
ios: Permissions.PERMISSIONS.IOS.MICROPHONE,
default: Permissions.PERMISSIONS.ANDROID.RECORD_AUDIO,
export const hasBluetoothPermission = async () => {
const bluetooth = Platform.select({
ios: Permissions.PERMISSIONS.IOS.BLUETOOTH_PERIPHERAL,
default: Permissions.PERMISSIONS.ANDROID.BLUETOOTH_CONNECT,
});
const hasPermission = await Permissions.check(targetSource);
switch (hasPermission) {
const hasBluetooth = await Permissions.check(bluetooth);
switch (hasBluetooth) {
case Permissions.RESULTS.DENIED:
case Permissions.RESULTS.UNAVAILABLE: {
const permissionRequest = await Permissions.request(targetSource);
const permissionRequest = await Permissions.request(bluetooth);
return permissionRequest === Permissions.RESULTS.GRANTED;
}
case Permissions.RESULTS.BLOCKED:
return false;
default:
return true;
}
};
export const hasMicrophonePermission = async () => {
const microphone = Platform.select({
ios: Permissions.PERMISSIONS.IOS.MICROPHONE,
default: Permissions.PERMISSIONS.ANDROID.RECORD_AUDIO,
});
const hasMicrophone = await Permissions.check(microphone);
switch (hasMicrophone) {
case Permissions.RESULTS.DENIED:
case Permissions.RESULTS.UNAVAILABLE: {
const permissionRequest = await Permissions.request(microphone);
return permissionRequest === Permissions.RESULTS.GRANTED;
}
case Permissions.RESULTS.BLOCKED:
return false;
default:
return true;
}
return true;
};

View file

@ -5,6 +5,7 @@ import {Alert} from 'react-native';
import {hasMicrophonePermission, joinCall, unmuteMyself} from '@calls/actions';
import {leaveCallPopCallScreen} from '@calls/actions/calls';
import {hasBluetoothPermission} from '@calls/actions/permissions';
import {
getCallsConfig,
getCallsState,
@ -192,6 +193,8 @@ const doJoinCall = async (
recordingAlertLock = false;
recordingWillBePostedLock = true; // only unlock if/when the user stops a recording.
await hasBluetoothPermission();
const hasPermission = await hasMicrophonePermission();
setMicPermissionsGranted(hasPermission);

View file

@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Pressable, type StyleProp, Text, type TextStyle, type ViewStyle} from 'react-native';
import {setSpeakerphoneOn} from '@calls/actions';
import CompassIcon from '@components/compass_icon';
import type {CurrentCall} from '@calls/types/calls';
type Props = {
pressableStyle: StyleProp<ViewStyle>;
iconStyle: StyleProp<ViewStyle>;
buttonTextStyle: StyleProp<TextStyle>;
currentCall: CurrentCall;
}
export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, currentCall}: Props) => {
const intl = useIntl();
const speakerLabel = intl.formatMessage({id: 'mobile.calls_speaker', defaultMessage: 'SpeakerPhone'});
const toggleSpeakerPhone = useCallback(() => {
setSpeakerphoneOn(!currentCall?.speakerphoneOn);
}, [currentCall?.speakerphoneOn]);
return (
<Pressable
style={pressableStyle}
onPress={toggleSpeakerPhone}
>
<CompassIcon
name={'volume-high'}
size={24}
style={iconStyle}
/>
<Text style={buttonTextStyle}>{speakerLabel}</Text>
</Pressable>
);
};

View file

@ -0,0 +1,124 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Pressable, type StyleProp, Text, type TextStyle, View, type ViewStyle} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {setPreferredAudioRoute} from '@calls/actions/calls';
import {AudioDevice, type CurrentCall} from '@calls/types/calls';
import CompassIcon from '@components/compass_icon';
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import {Device} from '@constants';
import {useTheme} from '@context/theme';
import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {typography} from '@utils/typography';
type Props = {
pressableStyle: StyleProp<ViewStyle>;
iconStyle: StyleProp<ViewStyle>;
buttonTextStyle: StyleProp<TextStyle>;
currentCall: CurrentCall;
}
const style = {
bold: typography('Body', 200, 'SemiBold'),
};
export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, currentCall}: Props) => {
const intl = useIntl();
const theme = useTheme();
const {bottom} = useSafeAreaInsets();
const isTablet = Device.IS_TABLET; // not `useIsTablet` because even if we're in splitView, we're still using a tablet.
const color = theme.awayIndicator;
const audioDeviceInfo = currentCall.audioDeviceInfo;
const phoneLabel = intl.formatMessage({id: 'mobile.calls_phone', defaultMessage: 'Phone'});
const tabletLabel = intl.formatMessage({id: 'mobile.calls_tablet', defaultMessage: 'Tablet'});
const speakerLabel = intl.formatMessage({id: 'mobile.calls_speaker', defaultMessage: 'SpeakerPhone'});
const bluetoothLabel = intl.formatMessage({id: 'mobile.calls_bluetooth', defaultMessage: 'Bluetooth'});
const deviceSelector = useCallback(async () => {
const currentDevice = audioDeviceInfo.selectedAudioDevice;
const available = audioDeviceInfo.availableAudioDeviceList;
const selectDevice = (device: AudioDevice) => {
setPreferredAudioRoute(device);
dismissBottomSheet();
};
const renderContent = () => {
return (
<View>
{available.includes(AudioDevice.Earpiece) && isTablet &&
<SlideUpPanelItem
icon={'tablet'}
onPress={() => selectDevice(AudioDevice.Earpiece)}
text={tabletLabel}
textStyles={currentDevice === AudioDevice.Earpiece ? {...style.bold, color} : {}}
/>
}
{available.includes(AudioDevice.Earpiece) && !isTablet &&
<SlideUpPanelItem
icon={'cellphone'}
onPress={() => selectDevice(AudioDevice.Earpiece)}
text={phoneLabel}
textStyles={currentDevice === AudioDevice.Earpiece ? {...style.bold, color} : {}}
/>
}
{available.includes(AudioDevice.Speakerphone) &&
<SlideUpPanelItem
icon={'volume-high'}
onPress={() => selectDevice(AudioDevice.Speakerphone)}
text={speakerLabel}
textStyles={currentDevice === AudioDevice.Speakerphone ? {...style.bold, color} : {}}
/>
}
{available.includes(AudioDevice.Bluetooth) &&
<SlideUpPanelItem
icon={'bluetooth'}
onPress={() => selectDevice(AudioDevice.Bluetooth)}
text={bluetoothLabel}
textStyles={currentDevice === AudioDevice.Bluetooth ? {...style.bold, color} : {}}
/>
}
</View>
);
};
await bottomSheet({
closeButtonId: 'close-other-actions',
renderContent,
snapPoints: [1, bottomSheetSnapPoint(audioDeviceInfo.availableAudioDeviceList.length + 1, ITEM_HEIGHT, bottom)],
title: intl.formatMessage({id: 'mobile.calls_audio_device', defaultMessage: 'Select audio device'}),
theme,
});
}, [setPreferredAudioRoute, audioDeviceInfo, color]);
let icon = 'volume-high';
let label = speakerLabel;
switch (audioDeviceInfo.selectedAudioDevice) {
case AudioDevice.Earpiece:
icon = isTablet ? 'tablet' : 'cellphone';
label = isTablet ? tabletLabel : phoneLabel;
break;
case AudioDevice.Bluetooth:
icon = 'bluetooth';
label = bluetoothLabel;
break;
}
return (
<Pressable
style={pressableStyle}
onPress={deviceSelector}
>
<CompassIcon
name={icon}
size={24}
style={iconStyle}
/>
<Text style={buttonTextStyle}>{label}</Text>
</Pressable>
);
};

View file

@ -3,25 +3,21 @@
import {RTCPeer} from '@mattermost/calls/lib';
import {deflate} from 'pako';
import {DeviceEventEmitter, type EmitterSubscription} from 'react-native';
import {DeviceEventEmitter, type EmitterSubscription, Platform} from 'react-native';
import InCallManager from 'react-native-incall-manager';
import {
MediaStream,
MediaStreamTrack,
mediaDevices,
RTCPeerConnection,
} from 'react-native-webrtc';
import {mediaDevices, MediaStream, MediaStreamTrack, RTCPeerConnection} from 'react-native-webrtc';
import {setSpeakerPhone} from '@calls/state';
import {setPreferredAudioRoute, setSpeakerphoneOn} from '@calls/actions/calls';
import {setAudioDeviceInfo} from '@calls/state';
import {AudioDevice, type AudioDeviceInfo, type AudioDeviceInfoRaw, type CallsConnection} from '@calls/types/calls';
import {getICEServersConfigs} from '@calls/utils';
import {WebsocketEvents} from '@constants';
import {getServerCredentials} from '@init/credentials';
import NetworkManager from '@managers/network_manager';
import {logError, logDebug, logWarning, logInfo} from '@utils/log';
import {logDebug, logError, logInfo, logWarning} from '@utils/log';
import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client';
import type {CallsConnection} from '@calls/types/calls';
import type {EmojiData} from '@mattermost/calls/lib/types';
const peerConnectTimeout = 5000;
@ -40,6 +36,7 @@ export async function newConnection(
let voiceTrack: MediaStreamTrack | null = null;
let isClosed = false;
let onCallEnd: EmitterSubscription | null = null;
let audioDeviceChanged: EmitterSubscription | null = null;
const streams: MediaStream[] = [];
const initializeVoiceTrack = async () => {
@ -97,6 +94,7 @@ export async function newConnection(
peer?.destroy();
peer = null;
InCallManager.stop();
audioDeviceChanged?.remove();
if (closeCb) {
closeCb();
@ -201,8 +199,36 @@ export async function newConnection(
}
}
InCallManager.start({media: 'video'});
setSpeakerPhone(true);
InCallManager.start();
InCallManager.stopProximitySensor();
let btInitialized = false;
let speakerInitialized = false;
audioDeviceChanged = DeviceEventEmitter.addListener('onAudioDeviceChanged', (data: AudioDeviceInfoRaw) => {
const info: AudioDeviceInfo = {
availableAudioDeviceList: JSON.parse(data.availableAudioDeviceList),
selectedAudioDevice: data.selectedAudioDevice,
};
setAudioDeviceInfo(info);
// Auto switch to bluetooth the first time we connect to bluetooth, but not after.
if (!btInitialized) {
if (info.availableAudioDeviceList.includes(AudioDevice.Bluetooth)) {
setPreferredAudioRoute(AudioDevice.Bluetooth);
btInitialized = true;
} else if (!speakerInitialized) {
// If we don't have bluetooth available, default to speakerphone on.
setPreferredAudioRoute(AudioDevice.Speakerphone);
speakerInitialized = true;
}
}
});
// We default to speakerphone (Android is handled above in the onAudioDeviceChanged handler above).
if (Platform.OS === 'ios') {
setSpeakerphoneOn(true);
}
peer = new RTCPeer({
iceServers: iceConfigs || [],

View file

@ -19,9 +19,10 @@ import {Navigation} from 'react-native-navigation';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {RTCView} from 'react-native-webrtc';
import {leaveCall, muteMyself, setSpeakerphoneOn, unmuteMyself} from '@calls/actions';
import {leaveCall, muteMyself, unmuteMyself} from '@calls/actions';
import {startCallRecording, stopCallRecording} from '@calls/actions/calls';
import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} from '@calls/alerts';
import {AudioDeviceButton} from '@calls/components/audio_device_button';
import CallAvatar from '@calls/components/call_avatar';
import CallDuration from '@calls/components/call_duration';
import CallsBadge, {CallsBadgeType} from '@calls/components/calls_badge';
@ -305,8 +306,14 @@ const CallScreen = ({
const callThreadOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Call Thread'});
const recordOptionTitle = intl.formatMessage({id: 'mobile.calls_record', defaultMessage: 'Record'});
const stopRecordingOptionTitle = intl.formatMessage({id: 'mobile.calls_stop_recording', defaultMessage: 'Stop Recording'});
const openChannelOptionTitle = intl.formatMessage({id: 'mobile.calls_open_channel', defaultMessage: 'Open Channel'});
const stopRecordingOptionTitle = intl.formatMessage({
id: 'mobile.calls_stop_recording',
defaultMessage: 'Stop Recording',
});
const openChannelOptionTitle = intl.formatMessage({
id: 'mobile.calls_open_channel',
defaultMessage: 'Open Channel',
});
useEffect(() => {
mergeNavigationOptions('Call', {
@ -349,10 +356,6 @@ const CallScreen = ({
setShowReactions((prev) => !prev);
}, [setShowReactions]);
const toggleSpeakerPhone = useCallback(() => {
setSpeakerphoneOn(!currentCall?.speakerphoneOn);
}, [currentCall?.speakerphoneOn]);
const toggleControlsInLandscape = useCallback(() => {
setShowControlsInLandscape(!showControlsInLandscape);
}, [showControlsInLandscape]);
@ -658,27 +661,17 @@ const CallScreen = ({
style={style.buttonText}
/>
</Pressable>
<Pressable
testID={'toggle-speakerphone'}
style={[style.button, isLandscape && style.buttonLandscape]}
onPress={toggleSpeakerPhone}
>
<CompassIcon
name={'volume-high'}
size={24}
style={[
style.buttonIcon,
isLandscape && style.buttonIconLandscape,
style.speakerphoneIcon,
currentCall.speakerphoneOn && style.buttonOn,
]}
/>
<FormattedText
id={'mobile.calls_speaker'}
defaultMessage={'Speaker'}
style={style.buttonText}
/>
</Pressable>
<AudioDeviceButton
pressableStyle={[style.button, isLandscape && style.buttonLandscape]}
iconStyle={[
style.buttonIcon,
isLandscape && style.buttonIconLandscape,
style.speakerphoneIcon,
currentCall.speakerphoneOn && style.buttonOn,
]}
buttonTextStyle={style.buttonText}
currentCall={currentCall}
/>
<Pressable
style={[style.button, isLandscape && style.buttonLandscape]}
onPress={toggleReactions}

View file

@ -7,7 +7,7 @@ import {act, renderHook} from '@testing-library/react-hooks';
import {needsRecordingAlert} from '@calls/alerts';
import {
newCurrentCall,
newCurrentCall, setAudioDeviceInfo,
setCallsState,
setChannelsWithCalls,
setCurrentCall,
@ -23,24 +23,25 @@ import {
userReacted,
} from '@calls/state';
import {
setCalls,
userJoinedCall,
userLeftCall,
callStarted,
callEnded,
setUserMuted,
setCallScreenOn,
setCallScreenOff,
setRaisedHand,
callStarted,
myselfLeftCall,
setCalls,
setCallScreenOff,
setCallScreenOn,
setChannelEnabled,
setScreenShareURL,
setSpeakerPhone,
setConfig,
setPluginEnabled,
setRaisedHand,
setScreenShareURL,
setSpeakerPhone,
setUserMuted,
setUserVoiceOn,
userJoinedCall,
userLeftCall,
} from '@calls/state/actions';
import {
AudioDevice,
type Call,
type CallsState,
type CurrentCall,
@ -768,6 +769,59 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[1], null);
});
it('setAudioDeviceInfo', () => {
const initialCallsState = {
...DefaultCallsState,
myUserId: 'myUserId',
calls: {'channel-1': call1, 'channel-2': call2},
};
const newCall1 = {
...call1,
participants: {
...call1.participants,
myUserId: {id: 'myUserId', muted: true, raisedHand: 0},
},
};
const expectedCallsState = {
...initialCallsState,
calls: {
...initialCallsState.calls,
'channel-1': newCall1,
},
};
const defaultAudioDeviceInfo = {
availableAudioDeviceList: [],
selectedAudioDevice: AudioDevice.None,
};
const newAudioDeviceInfo = {
availableAudioDeviceList: [AudioDevice.Speakerphone, AudioDevice.Earpiece],
selectedAudioDevice: AudioDevice.Speakerphone,
};
// setup
const {result} = renderHook(() => {
return [useCallsState('server1'), useCurrentCall()];
});
act(() => setCallsState('server1', initialCallsState));
assert.deepEqual(result.current[0], initialCallsState);
assert.deepEqual(result.current[1], null);
// test
act(() => newCurrentCall('server1', 'channel-1', 'myUserId'));
act(() => userJoinedCall('server1', 'channel-1', 'myUserId'));
assert.deepEqual((result.current[1] as CurrentCall | null)?.audioDeviceInfo, defaultAudioDeviceInfo);
act(() => setAudioDeviceInfo(newAudioDeviceInfo));
assert.deepEqual((result.current[1] as CurrentCall | null)?.audioDeviceInfo, newAudioDeviceInfo);
assert.deepEqual((result.current[1] as CurrentCall | null)?.speakerphoneOn, false);
assert.deepEqual(result.current[0], expectedCallsState);
act(() => {
myselfLeftCall();
});
assert.deepEqual(result.current[0], expectedCallsState);
assert.deepEqual(result.current[1], null);
});
it('MicPermissions', () => {
const initialGlobalState = DefaultGlobalCallsState;
const initialCallsState: CallsState = {

View file

@ -16,6 +16,7 @@ import {
setGlobalCallsState,
} from '@calls/state';
import {
type AudioDeviceInfo,
type Call,
type CallsConfigState,
type ChannelsWithCalls,
@ -419,6 +420,13 @@ export const setSpeakerPhone = (speakerphoneOn: boolean) => {
}
};
export const setAudioDeviceInfo = (info: AudioDeviceInfo) => {
const call = getCurrentCall();
if (call) {
setCurrentCall({...call, audioDeviceInfo: info});
}
};
export const setConfig = (serverUrl: string, config: Partial<CallsConfigState>) => {
const callsConfig = getCallsConfig(serverUrl);
setCallsConfig(serverUrl, {...callsConfig, ...config});

View file

@ -45,12 +45,21 @@ export const DefaultCall: Call = {
hostId: '',
};
export enum AudioDevice {
Earpiece = 'EARPIECE',
Speakerphone = 'SPEAKER_PHONE',
Bluetooth = 'BLUETOOTH',
WiredHeadset = 'WIRED_HEADSET',
None = 'NONE',
}
export type CurrentCall = Call & {
connected: boolean;
serverUrl: string;
myUserId: string;
screenShareURL: string;
speakerphoneOn: boolean;
audioDeviceInfo: AudioDeviceInfo;
voiceOn: Dictionary<boolean>;
micPermissionsErrorDismissed: boolean;
reactionStream: ReactionStreamEmoji[];
@ -64,6 +73,7 @@ export const DefaultCurrentCall: CurrentCall = {
myUserId: '',
screenShareURL: '',
speakerphoneOn: false,
audioDeviceInfo: {availableAudioDeviceList: [], selectedAudioDevice: AudioDevice.None},
voiceOn: {},
micPermissionsErrorDismissed: false,
reactionStream: [],
@ -147,3 +157,13 @@ export type ReactionStreamEmoji = {
count: number;
literal?: string;
};
export type AudioDeviceInfoRaw = {
availableAudioDeviceList: string;
selectedAudioDevice: AudioDevice;
};
export type AudioDeviceInfo = {
availableAudioDeviceList: AudioDevice[];
selectedAudioDevice: AudioDevice;
};

View file

@ -416,6 +416,8 @@
"mobile.android.photos_permission_denied_description": "Upload photos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo library.",
"mobile.android.photos_permission_denied_title": "{applicationName} would like to access your photos",
"mobile.announcement_banner.title": "Announcement",
"mobile.calls_audio_device": "Select audio device",
"mobile.calls_bluetooth": "Bluetooth",
"mobile.calls_call_ended": "Call ended",
"mobile.calls_call_screen": "Call",
"mobile.calls_call_thread": "Call Thread",
@ -463,6 +465,7 @@
"mobile.calls_participant_limit_title_GA": "This call is at capacity",
"mobile.calls_participant_rec": "The host has started recording this meeting. By staying in the meeting you give consent to being recorded.",
"mobile.calls_participant_rec_title": "Recording is in progress",
"mobile.calls_phone": "Phone",
"mobile.calls_raise_hand": "Raise hand",
"mobile.calls_react": "React",
"mobile.calls_rec": "rec",

14
package-lock.json generated
View file

@ -18,7 +18,7 @@
"@formatjs/intl-relativetimeformat": "11.2.1",
"@gorhom/bottom-sheet": "4.4.5",
"@mattermost/calls": "github:mattermost/calls-common#v0.12.0",
"@mattermost/compass-icons": "0.1.35",
"@mattermost/compass-icons": "0.1.36",
"@mattermost/react-native-emm": "1.3.5",
"@mattermost/react-native-network-client": "1.3.3",
"@mattermost/react-native-paste-input": "0.6.2",
@ -3426,9 +3426,9 @@
}
},
"node_modules/@mattermost/compass-icons": {
"version": "0.1.35",
"resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.35.tgz",
"integrity": "sha512-qCZjvU6Vsl7cg/0urrq8ItK5CXnt3DqjFq/w9mBpWuem/YhMWLMVBoSuASAx9S2HV7Vr/iMdsMGWH/4qXkXCBg=="
"version": "0.1.36",
"resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.36.tgz",
"integrity": "sha512-0v816WW2WmBxdXJxXUs6g1EP4xPF99tBxl7Uo8ocF0+E5T7y4YN7+bjz/pAme5eWuD6Yexc+otzkn+mWjqFU/A=="
},
"node_modules/@mattermost/react-native-emm": {
"version": "1.3.5",
@ -24273,9 +24273,9 @@
}
},
"@mattermost/compass-icons": {
"version": "0.1.35",
"resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.35.tgz",
"integrity": "sha512-qCZjvU6Vsl7cg/0urrq8ItK5CXnt3DqjFq/w9mBpWuem/YhMWLMVBoSuASAx9S2HV7Vr/iMdsMGWH/4qXkXCBg=="
"version": "0.1.36",
"resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.36.tgz",
"integrity": "sha512-0v816WW2WmBxdXJxXUs6g1EP4xPF99tBxl7Uo8ocF0+E5T7y4YN7+bjz/pAme5eWuD6Yexc+otzkn+mWjqFU/A=="
},
"@mattermost/react-native-emm": {
"version": "1.3.5",

View file

@ -15,7 +15,7 @@
"@formatjs/intl-relativetimeformat": "11.2.1",
"@gorhom/bottom-sheet": "4.4.5",
"@mattermost/calls": "github:mattermost/calls-common#v0.12.0",
"@mattermost/compass-icons": "0.1.35",
"@mattermost/compass-icons": "0.1.36",
"@mattermost/react-native-emm": "1.3.5",
"@mattermost/react-native-network-client": "1.3.3",
"@mattermost/react-native-paste-input": "0.6.2",

View file

@ -60,7 +60,7 @@ declare module 'react-native-incall-manager' {
getAudioUri(audioType: string, fileType: string): any;
chooseAudioRoute(route: any): any;
chooseAudioRoute(route: any): Promise<any>;
requestAudioFocus(): void;