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:
parent
30706382a0
commit
c1430757b8
14 changed files with 368 additions and 70 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
41
app/products/calls/components/audio_device_button.ios.tsx
Normal file
41
app/products/calls/components/audio_device_button.ios.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
124
app/products/calls/components/audio_device_button.tsx
Normal file
124
app/products/calls/components/audio_device_button.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 || [],
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
14
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue