2017-10-24 08:40:39 +00:00
|
|
|
import _ from 'lodash';
|
|
|
|
import React, { Component } from 'react';
|
2018-05-07 20:55:17 +00:00
|
|
|
import { NativeModules, Text, TouchableHighlight, View } from 'react-native';
|
2017-10-24 08:40:39 +00:00
|
|
|
|
2022-09-27 07:10:28 +00:00
|
|
|
import { BottomSheet, hideSheet } from '../../../base/dialog';
|
2022-06-14 10:20:16 +00:00
|
|
|
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
|
2017-11-20 11:13:42 +00:00
|
|
|
import { translate } from '../../../base/i18n';
|
2019-08-30 16:39:06 +00:00
|
|
|
import {
|
|
|
|
Icon,
|
2022-04-07 14:45:01 +00:00
|
|
|
IconCar,
|
2019-08-30 16:39:06 +00:00
|
|
|
IconDeviceBluetooth,
|
|
|
|
IconDeviceEarpiece,
|
|
|
|
IconDeviceHeadphone,
|
|
|
|
IconDeviceSpeaker
|
|
|
|
} from '../../../base/icons';
|
2019-03-21 16:38:29 +00:00
|
|
|
import { connect } from '../../../base/redux';
|
2022-06-14 10:20:16 +00:00
|
|
|
import { ColorPalette } from '../../../base/styles';
|
2018-05-07 20:55:17 +00:00
|
|
|
|
2018-12-18 11:18:54 +00:00
|
|
|
import styles from './styles';
|
2017-10-24 08:40:39 +00:00
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
const { AudioMode } = NativeModules;
|
|
|
|
|
2017-11-14 20:18:16 +00:00
|
|
|
/**
|
2018-05-07 20:55:17 +00:00
|
|
|
* Type definition for a single entry in the device list.
|
|
|
|
*/
|
|
|
|
type Device = {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Name of the icon which will be rendered on the right.
|
|
|
|
*/
|
2019-08-30 16:39:06 +00:00
|
|
|
icon: Object,
|
2018-05-07 20:55:17 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* True if the element is selected (will be highlighted in blue),
|
|
|
|
* false otherwise.
|
|
|
|
*/
|
|
|
|
selected: boolean,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Text which will be rendered in the row.
|
|
|
|
*/
|
|
|
|
text: string,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Device type.
|
|
|
|
*/
|
2019-08-09 10:41:52 +00:00
|
|
|
type: string,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unique device ID.
|
|
|
|
*/
|
|
|
|
uid: ?string
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* "Raw" device, as returned by native.
|
|
|
|
*/
|
|
|
|
type RawDevice = {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Display name for the device.
|
|
|
|
*/
|
|
|
|
name: ?string,
|
|
|
|
|
|
|
|
/**
|
2021-11-04 21:10:43 +00:00
|
|
|
* Is this device selected?
|
2019-08-09 10:41:52 +00:00
|
|
|
*/
|
|
|
|
selected: boolean,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Device type.
|
|
|
|
*/
|
|
|
|
type: string,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unique device ID.
|
|
|
|
*/
|
|
|
|
uid: ?string
|
2018-05-07 20:55:17 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@code AudioRoutePickerDialog}'s React {@code Component} prop types.
|
2017-11-14 20:18:16 +00:00
|
|
|
*/
|
|
|
|
type Props = {
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
/**
|
|
|
|
* Object describing available devices.
|
|
|
|
*/
|
|
|
|
_devices: Array<RawDevice>,
|
|
|
|
|
2017-11-14 20:18:16 +00:00
|
|
|
/**
|
|
|
|
* Used for hiding the dialog when the selection was completed.
|
|
|
|
*/
|
2017-11-20 11:13:42 +00:00
|
|
|
dispatch: Function,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invoked to obtain translated strings.
|
|
|
|
*/
|
|
|
|
t: Function
|
2017-11-14 20:18:16 +00:00
|
|
|
};
|
|
|
|
|
2018-05-07 20:55:17 +00:00
|
|
|
/**
|
|
|
|
* {@code AudioRoutePickerDialog}'s React {@code Component} state types.
|
|
|
|
*/
|
2017-11-14 20:18:16 +00:00
|
|
|
type State = {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Array of available devices.
|
|
|
|
*/
|
2018-05-07 20:55:17 +00:00
|
|
|
devices: Array<Device>
|
2017-11-14 20:18:16 +00:00
|
|
|
};
|
|
|
|
|
2017-10-24 08:40:39 +00:00
|
|
|
/**
|
|
|
|
* Maps each device type to a display name and icon.
|
|
|
|
*/
|
|
|
|
const deviceInfoMap = {
|
|
|
|
BLUETOOTH: {
|
2019-08-30 16:39:06 +00:00
|
|
|
icon: IconDeviceBluetooth,
|
2017-11-20 11:13:42 +00:00
|
|
|
text: 'audioDevices.bluetooth',
|
2017-10-24 08:40:39 +00:00
|
|
|
type: 'BLUETOOTH'
|
|
|
|
},
|
2022-04-07 14:45:01 +00:00
|
|
|
CAR: {
|
|
|
|
icon: IconCar,
|
|
|
|
text: 'audioDevices.car',
|
|
|
|
type: 'CAR'
|
|
|
|
},
|
2017-10-24 08:40:39 +00:00
|
|
|
EARPIECE: {
|
2019-08-30 16:39:06 +00:00
|
|
|
icon: IconDeviceEarpiece,
|
2017-11-20 11:13:42 +00:00
|
|
|
text: 'audioDevices.phone',
|
2017-10-24 08:40:39 +00:00
|
|
|
type: 'EARPIECE'
|
|
|
|
},
|
|
|
|
HEADPHONES: {
|
2019-08-30 16:39:06 +00:00
|
|
|
icon: IconDeviceHeadphone,
|
2017-11-20 11:13:42 +00:00
|
|
|
text: 'audioDevices.headphones',
|
2017-10-24 08:40:39 +00:00
|
|
|
type: 'HEADPHONES'
|
|
|
|
},
|
|
|
|
SPEAKER: {
|
2019-08-30 16:39:06 +00:00
|
|
|
icon: IconDeviceSpeaker,
|
2017-11-20 11:13:42 +00:00
|
|
|
text: 'audioDevices.speaker',
|
2017-10-24 08:40:39 +00:00
|
|
|
type: 'SPEAKER'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements a React {@code Component} which prompts the user when a password
|
|
|
|
* is required to join a conference.
|
|
|
|
*/
|
|
|
|
class AudioRoutePickerDialog extends Component<Props, State> {
|
|
|
|
state = {
|
2017-11-14 20:18:16 +00:00
|
|
|
/**
|
|
|
|
* Available audio devices, it will be set in
|
2019-08-09 10:41:52 +00:00
|
|
|
* {@link #getDerivedStateFromProps()}.
|
2017-11-14 20:18:16 +00:00
|
|
|
*/
|
2017-10-24 08:40:39 +00:00
|
|
|
devices: []
|
|
|
|
};
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
/**
|
|
|
|
* Implements React's {@link Component#getDerivedStateFromProps()}.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
static getDerivedStateFromProps(props: Props) {
|
2022-04-07 14:45:01 +00:00
|
|
|
const { _devices: devices, t } = props;
|
2019-08-09 10:41:52 +00:00
|
|
|
|
|
|
|
if (!devices) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const audioDevices = [];
|
|
|
|
|
|
|
|
for (const device of devices) {
|
|
|
|
const infoMap = deviceInfoMap[device.type];
|
2021-09-23 12:06:52 +00:00
|
|
|
|
|
|
|
// Skip devices with unknown type.
|
|
|
|
if (!infoMap) {
|
|
|
|
// eslint-disable-next-line no-continue
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-04-07 14:45:01 +00:00
|
|
|
let text = t(infoMap.text);
|
|
|
|
|
|
|
|
// iOS provides descriptive names for these, use it.
|
|
|
|
if ((device.type === 'BLUETOOTH' || device.type === 'CAR') && device.name) {
|
|
|
|
text = device.name;
|
|
|
|
}
|
2019-08-09 10:41:52 +00:00
|
|
|
|
|
|
|
if (infoMap) {
|
|
|
|
const info = {
|
|
|
|
...infoMap,
|
|
|
|
selected: Boolean(device.selected),
|
2022-04-07 14:45:01 +00:00
|
|
|
text,
|
2019-08-09 10:41:52 +00:00
|
|
|
uid: device.uid
|
|
|
|
};
|
|
|
|
|
|
|
|
audioDevices.push(info);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure devices is alphabetically sorted.
|
|
|
|
return {
|
|
|
|
devices: _.sortBy(audioDevices, 'text')
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-10-24 08:40:39 +00:00
|
|
|
/**
|
|
|
|
* Initializes a new {@code PasswordRequiredPrompt} instance.
|
|
|
|
*
|
|
|
|
* @param {Props} props - The read-only React {@code Component} props with
|
|
|
|
* which the new instance is to be initialized.
|
|
|
|
*/
|
2018-05-07 20:55:17 +00:00
|
|
|
constructor(props: Props) {
|
2017-10-24 08:40:39 +00:00
|
|
|
super(props);
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
// Trigger an initial update.
|
|
|
|
AudioMode.updateDeviceList && AudioMode.updateDeviceList();
|
2017-10-24 08:40:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-05-07 20:55:17 +00:00
|
|
|
* Builds and returns a function which handles the selection of a device
|
|
|
|
* on the sheet. The selected device will be used by {@code AudioMode}.
|
2017-10-24 08:40:39 +00:00
|
|
|
*
|
2018-05-07 20:55:17 +00:00
|
|
|
* @param {Device} device - Object representing the selected device.
|
2017-10-24 08:40:39 +00:00
|
|
|
* @private
|
2018-05-07 20:55:17 +00:00
|
|
|
* @returns {Function}
|
2017-10-24 08:40:39 +00:00
|
|
|
*/
|
2018-05-07 20:55:17 +00:00
|
|
|
_onSelectDeviceFn(device: Device) {
|
|
|
|
return () => {
|
2022-06-20 14:53:19 +00:00
|
|
|
this.props.dispatch(hideSheet());
|
2019-08-09 10:41:52 +00:00
|
|
|
AudioMode.setAudioDevice(device.uid || device.type);
|
2018-05-07 20:55:17 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders a single device.
|
|
|
|
*
|
|
|
|
* @param {Device} device - Object representing a single device.
|
|
|
|
* @private
|
|
|
|
* @returns {ReactElement}
|
|
|
|
*/
|
|
|
|
_renderDevice(device: Device) {
|
2019-08-30 16:39:06 +00:00
|
|
|
const { icon, selected, text } = device;
|
2018-05-07 20:55:17 +00:00
|
|
|
const selectedStyle = selected ? styles.selectedText : {};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<TouchableHighlight
|
|
|
|
key = { device.type }
|
|
|
|
onPress = { this._onSelectDeviceFn(device) }
|
2018-12-18 11:18:54 +00:00
|
|
|
underlayColor = { ColorPalette.overflowMenuItemUnderlay } >
|
2018-05-07 20:55:17 +00:00
|
|
|
<View style = { styles.deviceRow } >
|
|
|
|
<Icon
|
2019-08-30 16:39:06 +00:00
|
|
|
src = { icon }
|
2022-06-14 10:20:16 +00:00
|
|
|
style = { [ styles.deviceIcon, bottomSheetStyles.buttons.iconStyle, selectedStyle ] } />
|
|
|
|
<Text style = { [ styles.deviceText, bottomSheetStyles.buttons.labelStyle, selectedStyle ] } >
|
2018-05-07 20:55:17 +00:00
|
|
|
{ text }
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
</TouchableHighlight>
|
|
|
|
);
|
2017-10-24 08:40:39 +00:00
|
|
|
}
|
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
/**
|
|
|
|
* Renders a "fake" device row indicating there are no devices.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @returns {ReactElement}
|
|
|
|
*/
|
|
|
|
_renderNoDevices() {
|
2022-06-14 10:20:16 +00:00
|
|
|
const { t } = this.props;
|
2019-08-09 10:41:52 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<View style = { styles.deviceRow } >
|
|
|
|
<Icon
|
2019-08-30 16:39:06 +00:00
|
|
|
src = { deviceInfoMap.SPEAKER.icon }
|
2022-06-14 10:20:16 +00:00
|
|
|
style = { [ styles.deviceIcon, bottomSheetStyles.buttons.iconStyle ] } />
|
|
|
|
<Text style = { [ styles.deviceText, bottomSheetStyles.buttons.labelStyle ] } >
|
2019-08-09 10:41:52 +00:00
|
|
|
{ t('audioDevices.none') }
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-10-24 08:40:39 +00:00
|
|
|
/**
|
|
|
|
* Implements React's {@link Component#render()}.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
* @returns {ReactElement}
|
|
|
|
*/
|
|
|
|
render() {
|
2017-11-14 20:18:16 +00:00
|
|
|
const { devices } = this.state;
|
2019-08-09 10:41:52 +00:00
|
|
|
let content;
|
2017-11-14 20:18:16 +00:00
|
|
|
|
2019-08-09 10:41:52 +00:00
|
|
|
if (devices.length === 0) {
|
|
|
|
content = this._renderNoDevices();
|
|
|
|
} else {
|
|
|
|
content = this.state.devices.map(this._renderDevice, this);
|
2017-10-24 08:40:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2022-06-20 14:53:19 +00:00
|
|
|
<BottomSheet>
|
2019-08-09 10:41:52 +00:00
|
|
|
{ content }
|
2018-05-07 20:55:17 +00:00
|
|
|
</BottomSheet>
|
2017-10-24 08:40:39 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-22 09:25:22 +00:00
|
|
|
/**
|
|
|
|
* Maps part of the Redux state to the props of this component.
|
|
|
|
*
|
|
|
|
* @param {Object} state - The Redux state.
|
|
|
|
* @returns {Object}
|
|
|
|
*/
|
|
|
|
function _mapStateToProps(state) {
|
|
|
|
return {
|
2019-08-09 10:41:52 +00:00
|
|
|
_devices: state['features/mobile/audio-mode'].devices
|
2019-05-22 09:25:22 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-06-20 14:53:19 +00:00
|
|
|
export default translate(connect(_mapStateToProps)(AudioRoutePickerDialog));
|