From 27ee6625eee38914beadf484d6d63ed99f399cd8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 13:33:22 -0700 Subject: [PATCH 1/5] Implement the "Voice & Video" tab of new user settings --- res/css/_components.scss | 1 + .../settings/tabs/_VoiceSettingsTab.scss | 24 +++ src/CallMediaHandler.js | 12 ++ .../views/dialogs/UserSettingsDialog.js | 3 +- .../views/settings/tabs/VoiceSettingsTab.js | 177 ++++++++++++++++++ src/i18n/strings/en_EN.json | 22 ++- 6 files changed, 228 insertions(+), 11 deletions(-) create mode 100644 res/css/views/settings/tabs/_VoiceSettingsTab.scss create mode 100644 src/components/views/settings/tabs/VoiceSettingsTab.js diff --git a/res/css/_components.scss b/res/css/_components.scss index dcc5799329..25b7c9342e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -138,6 +138,7 @@ @import "./views/settings/tabs/_GeneralSettingsTab.scss"; @import "./views/settings/tabs/_PreferencesSettingsTab.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; +@import "./views/settings/tabs/_VoiceSettingsTab.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_IncomingCallbox.scss"; @import "./views/voip/_VideoView.scss"; diff --git a/res/css/views/settings/tabs/_VoiceSettingsTab.scss b/res/css/views/settings/tabs/_VoiceSettingsTab.scss new file mode 100644 index 0000000000..6a6556eccd --- /dev/null +++ b/res/css/views/settings/tabs/_VoiceSettingsTab.scss @@ -0,0 +1,24 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_VoiceSettingsTab .mx_Field select { + width: 100%; + max-width: 100%; +} + +.mx_VoiceSettingsTab .mx_Field { + margin-right: 100px; // align with the rest of the fields +} diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js index 2330f86b99..9a1c9d70b8 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -69,4 +69,16 @@ export default { SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); Matrix.setMatrixCallVideoInput(deviceId); }, + + getAudioOutput: function() { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); + }, + + getAudioInput: function() { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput"); + }, + + getVideoInput: function() { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput"); + }, }; diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 0c11b29456..7b09970f56 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -25,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import LabsSettingsTab from "../settings/tabs/LabsSettingsTab"; import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab"; import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab"; +import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab"; // TODO: Ditch this whole component export class TempTab extends React.Component { @@ -68,7 +69,7 @@ export default class UserSettingsDialog extends React.Component { tabs.push(new Tab( _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", -
Voice Test
, + , )); tabs.push(new Tab( _td("Security & Privacy"), diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js new file mode 100644 index 0000000000..03d244ac7a --- /dev/null +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -0,0 +1,177 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import {_t} from "../../../../languageHandler"; +import CallMediaHandler from "../../../../CallMediaHandler"; +import Field from "../../elements/Field"; +import AccessibleButton from "../../elements/AccessibleButton"; +import {SettingLevel} from "../../../../settings/SettingsStore"; +const Modal = require("../../../../Modal"); +const sdk = require("../../../../index"); +const MatrixClientPeg = require("../../../../MatrixClientPeg"); + +export default class VoiceSettingsTab extends React.Component { + constructor() { + super(); + + this.state = { + mediaDevices: null, + activeAudioOutput: null, + activeAudioInput: null, + activeVideoInput: null, + } + } + + componentWillMount(): void { + this._refreshMediaDevices(); + } + + _refreshMediaDevices = async () => { + this.setState({ + mediaDevices: await CallMediaHandler.getDevices(), + activeAudioOutput: CallMediaHandler.getAudioOutput(), + activeAudioInput: CallMediaHandler.getAudioInput(), + activeVideoInput: CallMediaHandler.getVideoInput(), + }); + }; + + _requestMediaPermissions = () => { + const getUserMedia = ( + window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia + ); + if (getUserMedia) { + return getUserMedia.apply(window.navigator, [ + { video: true, audio: true }, + this._refreshMediaDevices, + function() { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { + title: _t('No media permissions'), + description: _t('You may need to manually permit Riot to access your microphone/webcam'), + }); + }, + ]); + } + }; + + _setAudioOutput = (e) => { + CallMediaHandler.setAudioOutput(e.target.value); + }; + + _setAudioInput = (e) => { + CallMediaHandler.setAudioInput(e.target.value); + }; + + _setVideoInput = (e) => { + CallMediaHandler.setVideoInput(e.target.value); + }; + + _setForceTurn = (forced) => { + MatrixClientPeg.get().setForceTURN(forced); + }; + + _renderDeviceOptions(devices, category) { + return devices.map((d) => ); + } + + render() { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + + let requestButton = null; + let speakerDropdown = null; + let microphoneDropdown = null; + let webcamDropdown = null; + if (this.state.mediaDevices === false) { + requestButton = ( +
+ {_t("Missing media permissions, click the button below to request.")} + + {_t("Request media permissions")} + +
+ ); + } else if (this.state.mediaDevices) { + speakerDropdown =

{ _t('No Audio Outputs detected') }

; + microphoneDropdown =

{ _t('No Microphones detected') }

; + webcamDropdown =

{ _t('No Webcams detected') }

; + + const defaultOption = { + deviceId: '', + label: _t('Default Device'), + }; + const getDefaultDevice = (devices) => { + if (!devices.some((i) => i.deviceId === 'default')) { + devices.unshift(defaultOption); + return ''; + } else { + return 'default'; + } + }; + + const audioOutputs = this.state.mediaDevices.audiooutput.slice(0); + if (audioOutputs.length > 0) { + const defaultDevice = getDefaultDevice(audioOutputs); + speakerDropdown = ( + + {this._renderDeviceOptions(audioOutputs, 'audioOutput')} + + ); + } + + const audioInputs = this.state.mediaDevices.audioinput.slice(0); + if (audioInputs.length > 0) { + const defaultDevice = getDefaultDevice(audioInputs); + microphoneDropdown = ( + + {this._renderDeviceOptions(audioInputs, 'audioInput')} + + ); + } + + const videoInputs = this.state.mediaDevices.videoinput.slice(0); + if (videoInputs.length > 0) { + const defaultDevice = getDefaultDevice(videoInputs); + webcamDropdown = ( + + {this._renderDeviceOptions(videoInputs, 'videoInput')} + + ); + } + } + + // TODO: Make 'webRtcForceTURN' be positively worded + return ( +
+
{_t("Voice & Video")}
+
+ {requestButton} + {speakerDropdown} + {microphoneDropdown} + {webcamDropdown} + + +
+
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1ef41824d5..ab4e8b74c7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -450,6 +450,18 @@ "Timeline": "Timeline", "Advanced": "Advanced", "Autocomplete delay (ms)": "Autocomplete delay (ms)", + "No media permissions": "No media permissions", + "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", + "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", + "Request media permissions": "Request media permissions", + "No Audio Outputs detected": "No Audio Outputs detected", + "No Microphones detected": "No Microphones detected", + "No Webcams detected": "No Webcams detected", + "Default Device": "Default Device", + "Audio Output": "Audio Output", + "Microphone": "Microphone", + "Camera": "Camera", + "Voice & Video": "Voice & Video", "Cannot add any more widgets": "Cannot add any more widgets", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", "Add a widget": "Add a widget", @@ -1044,7 +1056,6 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", - "Voice & Video": "Voice & Video", "Security & Privacy": "Security & Privacy", "Help & About": "Help & About", "Visit old settings": "Visit old settings", @@ -1300,16 +1311,7 @@ "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", "Bulk Options": "Bulk Options", "Desktop specific": "Desktop specific", - "No media permissions": "No media permissions", - "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", "Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.", - "No Audio Outputs detected": "No Audio Outputs detected", - "No Microphones detected": "No Microphones detected", - "No Webcams detected": "No Webcams detected", - "Default Device": "Default Device", - "Audio Output": "Audio Output", - "Microphone": "Microphone", - "Camera": "Camera", "VoIP": "VoIP", "Email": "Email", "Add email address": "Add email address", From a4e0796bfe8ebc0a40ead49ba30a0c92256c47a5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 13:37:20 -0700 Subject: [PATCH 2/5] Swap out the media permissions text for a paragraph element The sizing ratio looks way off when the text is just subtext. --- src/components/views/settings/tabs/VoiceSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index 03d244ac7a..7ad7614294 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -98,7 +98,7 @@ export default class VoiceSettingsTab extends React.Component { if (this.state.mediaDevices === false) { requestButton = (
- {_t("Missing media permissions, click the button below to request.")} +

{_t("Missing media permissions, click the button below to request.")}

{_t("Request media permissions")} From 3e09cd1d2e4cf79cdb06f71df7167a4e28081d55 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 13:43:16 -0700 Subject: [PATCH 3/5] Appease the linter --- src/components/views/settings/tabs/VoiceSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index 7ad7614294..9624b00bcf 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -33,7 +33,7 @@ export default class VoiceSettingsTab extends React.Component { activeAudioOutput: null, activeAudioInput: null, activeVideoInput: null, - } + }; } componentWillMount(): void { From 87cea5ef09b011744b0c7354ce9988a72a0cbbff Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 14:41:00 -0700 Subject: [PATCH 4/5] Fix sizings and margins --- res/css/views/elements/_AccessibleButton.scss | 2 +- res/css/views/settings/tabs/_VoiceSettingsTab.scss | 4 ++++ src/components/views/settings/tabs/VoiceSettingsTab.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index a6e884e170..8fca0b6128 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -27,7 +27,7 @@ limitations under the License. } .mx_AccessibleButton_hasKind { - padding: 10px 25px; + padding: 7px 18px; text-align: center; border-radius: 4px; display: inline-block; diff --git a/res/css/views/settings/tabs/_VoiceSettingsTab.scss b/res/css/views/settings/tabs/_VoiceSettingsTab.scss index 6a6556eccd..345fefe3cd 100644 --- a/res/css/views/settings/tabs/_VoiceSettingsTab.scss +++ b/res/css/views/settings/tabs/_VoiceSettingsTab.scss @@ -22,3 +22,7 @@ limitations under the License. .mx_VoiceSettingsTab .mx_Field { margin-right: 100px; // align with the rest of the fields } + +.mx_VoiceSettingsTab_missingMediaPermissions { + margin-bottom: 15px; +} diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index 9624b00bcf..f03711fb96 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -97,7 +97,7 @@ export default class VoiceSettingsTab extends React.Component { let webcamDropdown = null; if (this.state.mediaDevices === false) { requestButton = ( -
+

{_t("Missing media permissions, click the button below to request.")}

{_t("Request media permissions")} From 0f896684468ce07efcd66d84e536c60ab81617a4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 24 Jan 2019 14:42:30 -0700 Subject: [PATCH 5/5] Also the font size --- res/css/views/elements/_AccessibleButton.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 8fca0b6128..8de0b82554 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -31,6 +31,7 @@ limitations under the License. text-align: center; border-radius: 4px; display: inline-block; + font-size: 14px; } .mx_AccessibleButton_kind_primary {