From 8481a7b0e1aa583b0fd978f97124f10509c20e8a Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 15 Nov 2018 20:51:52 +0100 Subject: [PATCH 01/25] Jitsi Push-to-Talk --- src/PushToTalk.js | 50 + src/WidgetMessaging.js | 40 + src/components/structures/UserSettings.js | 1632 +++++++++++++++++ .../views/elements/PersistedElement.js | 17 +- src/settings/Settings.js | 19 +- 5 files changed, 1752 insertions(+), 6 deletions(-) create mode 100644 src/PushToTalk.js create mode 100644 src/components/structures/UserSettings.js diff --git a/src/PushToTalk.js b/src/PushToTalk.js new file mode 100644 index 00000000000..378fc9fe937 --- /dev/null +++ b/src/PushToTalk.js @@ -0,0 +1,50 @@ +/* +Copyright 2018 Andrew Morgan + +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 PlatformPeg from './PlatformPeg'; +import ActiveWidgetStore from './stores/ActiveWidgetStore'; +import SettingsStore from './settings/SettingsStore'; + +export function enable(keybinding) { + const id = 'pushToTalk'; + PlatformPeg.get().addGlobalKeybinding(id, keybinding, () => { + const widgetId = ActiveWidgetStore.getPersistentWidgetId(); + + // Only try to un/mute if jitsi is onscreen + if (widgetId === null || widgetId === undefined) { + return; + } + + const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); + widgetMessaging.unmuteJitsiAudio(); + }, () => { + const widgetId = ActiveWidgetStore.getPersistentWidgetId(); + + // Only try to un/mute if jitsi is onscreen + if (widgetId === null || widgetId === undefined) { + return; + } + + const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); + widgetMessaging.muteJitsiAudio(); + }); +} + +export function disable() { + const id = 'pushToTalk'; + const keybinding = SettingsStore.getValue(id).keybinding; + PlatformPeg.get().removeGlobalKeybinding(id, keybinding); +} diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 5b722df65f9..b211b96cddb 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -35,6 +35,7 @@ const OUTBOUND_API_NAME = 'toWidget'; export default class WidgetMessaging { constructor(widgetId, widgetUrl, target) { + console.log("I'm alive! My URL is:", widgetUrl) this.widgetId = widgetId; this.widgetUrl = widgetUrl; this.target = target; @@ -96,6 +97,45 @@ export default class WidgetMessaging { }); } + /** + * Toggle Jitsi Audio Mute + * @return {Promise} To be resolved when action completed + */ + toggleJitsiAudio() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: "audioMuteToggle", + }).then((response) => { + return response.success; + }); + } + + /** + * Jitsi Audio Mute + * @return {Promise} To be resolved when action completed + */ + muteJitsiAudio() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: "audioMute", + }).then((response) => { + return response.success; + }); + } + + /** + * Jitsi Audio Unmute + * @return {Promise} To be resolved when action completed + */ + unmuteJitsiAudio() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: "audioUnmute", + }).then((response) => { + return response.success; + }); + } + sendVisibility(visible) { return this.messageToWidget({ api: OUTBOUND_API_NAME, diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js new file mode 100644 index 00000000000..6375bfae4c7 --- /dev/null +++ b/src/components/structures/UserSettings.js @@ -0,0 +1,1632 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2017, 2018 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 SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; + +const React = require('react'); +const ReactDOM = require('react-dom'); +import PropTypes from 'prop-types'; +const sdk = require('../../index'); +const MatrixClientPeg = require("../../MatrixClientPeg"); +const PlatformPeg = require("../../PlatformPeg"); +const Modal = require('../../Modal'); +const dis = require("../../dispatcher"); +import sessionStore from '../../stores/SessionStore'; +import Promise from 'bluebird'; +const packageJson = require('../../../package.json'); +const UserSettingsStore = require('../../UserSettingsStore'); +const CallMediaHandler = require('../../CallMediaHandler'); +const Email = require('../../email'); +const AddThreepid = require('../../AddThreepid'); +const SdkConfig = require('../../SdkConfig'); +import Analytics from '../../Analytics'; +import AccessibleButton from '../views/elements/AccessibleButton'; +import { _t, _td } from '../../languageHandler'; +import * as languageHandler from '../../languageHandler'; +import * as FormattingUtils from '../../utils/FormattingUtils'; +import * as PushToTalk from '../../PushToTalk'; + +// if this looks like a release, use the 'version' from package.json; else use +// the git sha. Prepend version with v, to look like riot-web version +const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || ''; + +// Simple method to help prettify GH Release Tags and Commit Hashes. +const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i; +const gHVersionLabel = function(repo, token='') { + const match = token.match(semVerRegex); + let url; + if (match && match[1]) { // basic semVer string possibly with commit hash + url = (match.length > 1 && match[2]) + ? `https://github.com/${repo}/commit/${match[2]}` + : `https://github.com/${repo}/releases/tag/v${match[1]}`; + } else { + url = `https://github.com/${repo}/commit/${token.split('-')[0]}`; + } + return { token }; +}; + +// Enumerate some simple 'flip a bit' UI settings (if any). The strings provided here +// must be settings defined in SettingsStore. +const SIMPLE_SETTINGS = [ + { id: "urlPreviewsEnabled" }, + { id: "autoplayGifsAndVideos" }, + { id: "alwaysShowEncryptionIcons" }, + { id: "hideReadReceipts" }, + { id: "dontSendTypingNotifications" }, + { id: "alwaysShowTimestamps" }, + { id: "showTwelveHourTimestamps" }, + { id: "hideJoinLeaves" }, + { id: "hideAvatarChanges" }, + { id: "hideDisplaynameChanges" }, + { id: "useCompactLayout" }, + { id: "hideRedactions" }, + { id: "enableSyntaxHighlightLanguageDetection" }, + { id: "MessageComposerInput.autoReplaceEmoji" }, + { id: "MessageComposerInput.dontSuggestEmoji" }, + { id: "Pill.shouldHidePillAvatar" }, + { id: "TextualBody.disableBigEmoji" }, + { id: "VideoView.flipVideoHorizontally" }, + { id: "TagPanel.disableTagPanel" }, + { id: "enableWidgetScreenshots" }, + { id: "RoomSubList.showEmpty" }, + { id: "pinMentionedRooms" }, + { id: "pinUnreadRooms" }, + { id: "showDeveloperTools" }, +]; + +// These settings must be defined in SettingsStore +const ANALYTICS_SETTINGS = [ + { + id: 'analyticsOptIn', + fn: function(checked) { + checked ? Analytics.enable() : Analytics.disable(); + }, + }, +]; + +// These settings must be defined in SettingsStore +const WEBRTC_SETTINGS = [ + { + id: 'webRtcForceTURN', + fn: (val) => { + MatrixClientPeg.get().setForceTURN(val); + }, + }, +]; + +// These settings must be defined in SettingsStore +const CRYPTO_SETTINGS = [ + { + id: 'blacklistUnverifiedDevices', + fn: function(checked) { + MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); + }, + }, +]; + +// Enumerate the available themes, with a nice human text label. +// 'label' is how we describe it in the UI. +// 'value' is the value for the theme setting +// +// XXX: Ideally we would have a theme manifest or something and they'd be nicely +// packaged up in a single directory, and/or located at the application layer. +// But for now for expedience we just hardcode them here. +const THEMES = [ + { label: _td('Light theme'), value: 'light' }, + { label: _td('Dark theme'), value: 'dark' }, + { label: _td('Status.im theme'), value: 'status' }, +]; + +const IgnoredUser = React.createClass({ + propTypes: { + userId: PropTypes.string.isRequired, + onUnignored: PropTypes.func.isRequired, + }, + + _onUnignoreClick: function() { + const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers(); + const index = ignoredUsers.indexOf(this.props.userId); + if (index !== -1) { + ignoredUsers.splice(index, 1); + MatrixClientPeg.get().setIgnoredUsers(ignoredUsers) + .then(() => this.props.onUnignored(this.props.userId)); + } else this.props.onUnignored(this.props.userId); + }, + + render: function() { + return ( +
  • + + { _t("Unignore") } + + { this.props.userId } +
  • + ); + }, +}); + +let listenKeydown; +let listenKeyup; + +module.exports = React.createClass({ + displayName: 'UserSettings', + + propTypes: { + onClose: PropTypes.func, + // The brand string given when creating email pushers + brand: PropTypes.string, + + // The base URL to use in the referral link. Defaults to window.location.origin. + referralBaseUrl: PropTypes.string, + + // Team token for the referral link. If falsy, the referral section will + // not appear + teamToken: PropTypes.string, + }, + + getDefaultProps: function() { + return { + onClose: function() {}, + }; + }, + + getInitialState: function() { + return { + avatarUrl: null, + threepids: [], + phase: "UserSettings.LOADING", // LOADING, DISPLAY + email_add_pending: false, + vectorVersion: undefined, + rejectingInvites: false, + mediaDevices: null, + ignoredUsers: [], + pushToTalkAscii: SettingsStore.getValue('pushToTalk').ascii, + pushToTalkKeybinding: SettingsStore.getValue('pushToTalk').keybinding, + pushToTalkEnabled: SettingsStore.getValue('pushToTalk').enabled, + }; + }, + + componentWillMount: function() { + this._unmounted = false; + this._addThreepid = null; + + if (PlatformPeg.get()) { + Promise.resolve().then(() => { + return PlatformPeg.get().getAppVersion(); + }).done((appVersion) => { + if (this._unmounted) return; + this.setState({ + vectorVersion: appVersion, + }); + }, (e) => { + console.log("Failed to fetch app version", e); + }); + } + + this._refreshMediaDevices(); + this._refreshIgnoredUsers(); + + // Bulk rejecting invites: + // /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms() + // will still return rooms with invites. To get around this, add a listener for + // membership updates and kick the UI. + MatrixClientPeg.get().on("RoomMember.membership", this._onInviteStateChange); + + dis.dispatch({ + action: 'panel_disable', + sideDisabled: true, + middleDisabled: true, + }); + this._refreshFromServer(); + + if (PlatformPeg.get().isElectron()) { + const {ipcRenderer} = require('electron'); + + ipcRenderer.on('settings', this._electronSettings); + ipcRenderer.send('settings_get'); + } + + this.setState({ + language: languageHandler.getCurrentLanguage(), + }); + + this._sessionStore = sessionStore; + this._sessionStoreToken = this._sessionStore.addListener( + this._setStateFromSessionStore, + ); + this._setStateFromSessionStore(); + }, + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + this._me = MatrixClientPeg.get().credentials.userId; + }, + + componentWillUnmount: function() { + this._unmounted = true; + dis.dispatch({ + action: 'panel_disable', + sideDisabled: false, + middleDisabled: false, + }); + dis.unregister(this.dispatcherRef); + const cli = MatrixClientPeg.get(); + if (cli) { + cli.removeListener("RoomMember.membership", this._onInviteStateChange); + } + + if (PlatformPeg.get().isElectron()) { + const {ipcRenderer} = require('electron'); + ipcRenderer.removeListener('settings', this._electronSettings); + + // Stop recording push-to-talk shortcut if Settings window is closed + this._stopRecordingGlobalShortcut(); + } + }, + + // `UserSettings` assumes that the client peg will not be null, so give it some + // sort of assurance here by only allowing a re-render if the client is truthy. + // + // This is required because `UserSettings` maintains its own state and if this state + // updates (e.g. during _setStateFromSessionStore) after the client peg has been made + // null (during logout), then it will attempt to re-render and throw errors. + shouldComponentUpdate: function() { + return Boolean(MatrixClientPeg.get()); + }, + + _setStateFromSessionStore: function() { + this.setState({ + userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), + }); + }, + + _electronSettings: function(ev, settings) { + this.setState({ electron_settings: settings }); + }, + + _refreshMediaDevices: function(stream) { + if (stream) { + // kill stream so that we don't leave it lingering around with webcam enabled etc + // as here we called gUM to ask user for permission to their device names only + stream.getTracks().forEach((track) => track.stop()); + } + + Promise.resolve().then(() => { + return CallMediaHandler.getDevices(); + }).then((mediaDevices) => { + // console.log("got mediaDevices", mediaDevices, this._unmounted); + if (this._unmounted) return; + this.setState({ + mediaDevices, + activeAudioOutput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audiooutput'), + activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'), + activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'), + }); + }); + }, + + _refreshFromServer: function() { + const self = this; + Promise.all([ + UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(), + ]).done(function(resps) { + self.setState({ + avatarUrl: resps[0].avatar_url, + threepids: resps[1].threepids, + phase: "UserSettings.DISPLAY", + }); + }, function(error) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to load user settings: " + error); + Modal.createTrackedDialog('Can\'t load user settings', '', ErrorDialog, { + title: _t("Can't load user settings"), + description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")), + }); + }); + }, + + _refreshIgnoredUsers: function(userIdUnignored=null) { + const users = MatrixClientPeg.get().getIgnoredUsers(); + if (userIdUnignored) { + const index = users.indexOf(userIdUnignored); + if (index !== -1) users.splice(index, 1); + } + this.setState({ + ignoredUsers: users, + }); + }, + + onAction: function(payload) { + if (payload.action === "notifier_enabled") { + this.forceUpdate(); + } else if (payload.action === "ignore_state_changed") { + this._refreshIgnoredUsers(); + } + }, + + onAvatarPickerClick: function(ev) { + if (this.refs.file_label) { + this.refs.file_label.click(); + } + }, + + onAvatarSelected: function(ev) { + const self = this; + const changeAvatar = this.refs.changeAvatar; + if (!changeAvatar) { + console.error("No ChangeAvatar found to upload image to!"); + return; + } + changeAvatar.onFileSelected(ev).done(function() { + // dunno if the avatar changed, re-check it. + self._refreshFromServer(); + }, function(err) { + // const errMsg = (typeof err === "string") ? err : (err.error || ""); + console.error("Failed to set avatar: " + err); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, { + title: _t("Failed to set avatar."), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); + }, + + onAvatarRemoveClick: function() { + MatrixClientPeg.get().setAvatarUrl(null); + this.setState({avatarUrl: null}); // the avatar update will complete async for us + }, + + onLogoutClicked: function(ev) { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, { + title: _t("Sign out"), + description: +
    + { _t("For security, logging out will delete any end-to-end " + + "encryption keys from this browser. If you want to be able " + + "to decrypt your conversation history from future Riot sessions, " + + "please export your room keys for safe-keeping.") } +
    , + button: _t("Sign out"), + extraButtons: [ + , + ], + onFinished: (confirmed) => { + if (confirmed) { + dis.dispatch({action: 'logout'}); + if (this.props.onFinished) { + this.props.onFinished(); + } + } + }, + }); + }, + + onPasswordChangeError: function(err) { + let errMsg = err.error || ""; + if (err.httpStatus === 403) { + errMsg = _t("Failed to change password. Is your password correct?"); + } else if (err.httpStatus) { + errMsg += ` (HTTP status ${err.httpStatus})`; + } + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to change password: " + errMsg); + Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, { + title: _t("Error"), + description: errMsg, + }); + }, + + onPasswordChanged: function() { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Password changed', '', ErrorDialog, { + title: _t("Success"), + description: _t( + "Your password was successfully changed. You will not receive " + + "push notifications on other devices until you log back in to them", + ) + ".", + }); + }, + + _onAddEmailEditFinished: function(value, shouldSubmit) { + if (!shouldSubmit) return; + this._addEmail(); + }, + + _addEmail: function() { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + const emailAddress = this.refs.add_email_input.value; + if (!Email.looksValid(emailAddress)) { + Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, { + title: _t("Invalid Email Address"), + description: _t("This doesn't appear to be a valid email address"), + }); + return; + } + this._addThreepid = new AddThreepid(); + // we always bind emails when registering, so let's do the + // same here. + this._addThreepid.addEmailAddress(emailAddress, true).done(() => { + Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { + title: _t("Verification Pending"), + description: _t( + "Please check your email and click on the link it contains. Once this " + + "is done, click continue.", + ), + button: _t('Continue'), + onFinished: this.onEmailDialogFinished, + }); + }, (err) => { + this.setState({email_add_pending: false}); + console.error("Unable to add email address " + emailAddress + " " + err); + Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, { + title: _t("Unable to add email address"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); + ReactDOM.findDOMNode(this.refs.add_email_input).blur(); + this.setState({email_add_pending: true}); + }, + + onRemoveThreepidClicked: function(threepid) { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Remove 3pid', '', QuestionDialog, { + title: _t("Remove Contact Information?"), + description: _t("Remove %(threePid)s?", { threePid: threepid.address }), + button: _t('Remove'), + onFinished: (submit) => { + if (submit) { + this.setState({ + phase: "UserSettings.LOADING", + }); + MatrixClientPeg.get().deleteThreePid(threepid.medium, threepid.address).then(() => { + return this._refreshFromServer(); + }).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to remove contact information: " + err); + Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, { + title: _t("Unable to remove contact information"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }).done(); + } + }, + }); + }, + + onEmailDialogFinished: function(ok) { + if (ok) { + this.verifyEmailAddress(); + } else { + this.setState({email_add_pending: false}); + } + }, + + verifyEmailAddress: function() { + this._addThreepid.checkEmailLinkClicked().done(() => { + this._addThreepid = null; + this.setState({ + phase: "UserSettings.LOADING", + }); + this._refreshFromServer(); + this.setState({email_add_pending: false}); + }, (err) => { + this.setState({email_add_pending: false}); + if (err.errcode == 'M_THREEPID_AUTH_FAILED') { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const message = _t("Unable to verify email address.") + " " + + _t("Please check your email and click on the link it contains. Once this is done, click continue."); + Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { + title: _t("Verification Pending"), + description: message, + button: _t('Continue'), + onFinished: this.onEmailDialogFinished, + }); + } else { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to verify email address: " + err); + Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, { + title: _t("Unable to verify email address."), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + }); + }, + + _onDeactivateAccountClicked: function() { + const DeactivateAccountDialog = sdk.getComponent("dialogs.DeactivateAccountDialog"); + Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {}); + }, + + _onBugReportClicked: function() { + const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); + if (!BugReportDialog) { + return; + } + Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); + }, + + _onClearCacheClicked: function() { + if (!PlatformPeg.get()) return; + + MatrixClientPeg.get().stopClient(); + MatrixClientPeg.get().store.deleteAllData().done(() => { + PlatformPeg.get().reload(); + }); + }, + + _onInviteStateChange: function(event, member, oldMembership) { + if (member.userId === this._me && oldMembership === "invite") { + this.forceUpdate(); + } + }, + + _onRejectAllInvitesClicked: function(rooms, ev) { + this.setState({ + rejectingInvites: true, + }); + // reject the invites + const promises = rooms.map((room) => { + return MatrixClientPeg.get().leave(room.roomId).catch((e) => { + // purposefully drop errors to the floor: we'll just have a non-zero number on the UI + // after trying to reject all the invites. + }); + }); + Promise.all(promises).then(() => { + this.setState({ + rejectingInvites: false, + }); + }); + }, + + _onExportE2eKeysClicked: function() { + Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => { + require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { + cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog')); + }, "e2e-export"); + }, { + matrixClient: MatrixClientPeg.get(), + }); + }, + + _onImportE2eKeysClicked: function() { + Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => { + require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => { + cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog')); + }, "e2e-export"); + }, { + matrixClient: MatrixClientPeg.get(), + }); + }, + + _renderGroupSettings: function() { + const GroupUserSettings = sdk.getComponent('groups.GroupUserSettings'); + return ; + }, + + _renderReferral: function() { + const teamToken = this.props.teamToken; + if (!teamToken) { + return null; + } + if (typeof teamToken !== 'string') { + console.warn('Team token not a string'); + return null; + } + const href = (this.props.referralBaseUrl || window.location.origin) + + `/#/register?referrer=${this._me}&team_token=${teamToken}`; + return ( +
    +

    Referral

    +
    + { _t("Refer a friend to Riot:") } { href } +
    +
    + ); + }, + + onLanguageChange: function(newLang) { + if (this.state.language !== newLang) { + SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); + this.setState({ + language: newLang, + }); + PlatformPeg.get().reload(); + } + }, + + _renderLanguageSetting: function() { + const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); + return
    + + +
    ; + }, + + _translateKeybinding(key) { + // Custom translations to make keycodes look nicer + + // KeyA -> A + if (key.startsWith('Key')) { + key = key.substring(3); + } + + return key; + }, + + _startRecordingGlobalShortcut(asciiStateKey, codeStateKey) { + const keyAscii = []; + const keyCodes = []; + const self = this; + + // Record keypresses using KeyboardEvent + // Used for displaying ascii-representation of current keys + // in the UI + listenKeydown = function(event) { + // TODO: Show RightShift and things + const key = self._translateKeybinding(event.code); + const index = keyAscii.indexOf(key); + if (index === -1) { + keyAscii.push(key); + self.setState({pushToTalkAscii: keyAscii.join(' + ')}); + } + event.preventDefault(); + }; + listenKeyup = function(event) { + const index = keyAscii.indexOf(self._translateKeybinding(event.code)); + if (index !== -1) { + keyAscii.splice(index, 1); + } + event.preventDefault(); + }; + + window.addEventListener("keydown", listenKeydown); + window.addEventListener("keyup", listenKeyup); + + // Record keypresses using iohook + // Used for getting keycode-representation of current keys + // for later global shortcut registration + const {ipcRenderer} = require('electron'); + ipcRenderer.send('start-listening-keys'); + + // When a key is pressed, add all current pressed keys to the shortcut + // When a key is lifted, don't remove it from the shortcut + + // This enables a nicer shortcut-recording experience, as the user can + // press down their desired keys, release them, and then save the + // shortcut without all the keys disappearing + ipcRenderer.on('keypress', function(ev, event) { + if (event.keydown) { + const index = keyCodes.indexOf(event.keycode); + if (index === -1) { + keyCodes.push(event.keycode); + // slice is needed here to save a new copy of the keycodes + // array to the state, else if we update keycodes later, it + // still updates the state since the state has a ref to this array + self.setState({pushToTalkKeybinding: keyCodes.slice()}); + } + } else { + const index = keyCodes.indexOf(event.keycode); + if (index !== -1) { + keyCodes.splice(index, 1); + } + } + }); + + // Stop recording shortcut if window loses focus + ipcRenderer.on('window-blurred', () => { + if (this.state.settingKeybinding) { + this._stopRecordingGlobalShortcut(); + } + }); + }, + + _stopRecordingGlobalShortcut() { + // Stop recording KeyboardEvent keypresses + window.removeEventListener("keydown", listenKeydown); + window.removeEventListener("keyup", listenKeyup); + + // Stop recording iohook keypresses + const {ipcRenderer} = require('electron'); + ipcRenderer.send('stop-listening-keys'); + + this.setState({settingKeybinding: false}); + }, + + _onSetPushToTalkClicked: function() { + // Either record or save a new shortcut + const id = 'pushToTalk'; + const currentPTTState = SettingsStore.getValue(id); + + // Determine if we're reading shortcuts or setting them + if (!this.state.settingKeybinding) { + // Start listening for keypresses and show current + // held shortcut on screen + // Run some sort of function that just loops until the state changes back to + // not setting + this.state.pushToTalkAscii = 'Press Keys'; + this._startRecordingGlobalShortcut('pushToTalkAscii', 'pushToTalkKeybinding'); + } else { + this._stopRecordingGlobalShortcut(); + + // Disable and unregister old shortcut + PushToTalk.disable(); + + // Set the keybinding they've currently selected + currentPTTState.keybinding = this.state.pushToTalkKeybinding; + currentPTTState.ascii = this.state.pushToTalkAscii; + + // Update push to talk keybinding + SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); + + // Enable and register new shortcut + PushToTalk.enable(currentPTTState.keybinding); + } + + // Toggle setting state + this.setState({settingKeybinding: !this.state.settingKeybinding}); + }, + + _onTogglePushToTalkClicked: function(e) { + // Enable or disable push to talk functionality + const id = 'pushToTalk'; + const currentPTTState = SettingsStore.getValueAt(SettingLevel.DEVICE, id); + + if (e.target.checked) { + // Enable push to talk + + this.setState({pushToTalkEnabled: true}); + currentPTTState.enabled = true; + SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); + + PushToTalk.enable(currentPTTState.keybinding); + } else { + // Disable push to talk + console.log("Disabling push to talk...") + + this.setState({pushToTalkEnabled: false}); + currentPTTState.enabled = false; + SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); + this.setState({pushToTalkKeybinding: []}); + + PushToTalk.disable(); + } + }, + + _renderPushToTalkSettings: function() { + const id = "pushToTalk"; + const buttonLabel = this.state.settingKeybinding ? 'Stop' : 'Set'; + const activated = SettingsStore.getValueAt(SettingLevel.DEVICE, id).enabled; + + return ( +
    + + + + + + + + +
    + + + {"Shortcut: " + this.state.pushToTalkAscii} + +
    +
    + ); + }, + + _renderUserInterfaceSettings: function() { + // TODO: this ought to be a separate component so that we don't need + // to rebind the onChange each time we render + const onChange = (e) => + SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); + return ( +
    +

    { _t("User Interface") }

    +
    + { SIMPLE_SETTINGS.map( this._renderAccountSetting ) } + { THEMES.map( this._renderThemeOption ) } + + + + + + + +
    { _t('Autocomplete Delay (ms):') } + +
    + { this._renderLanguageSetting() } +
    +
    + ); + }, + + _renderAccountSetting: function(setting) { + const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); + return ( +
    + +
    + ); + }, + + _renderThemeOption: function(setting) { + const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); + const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value}); + return ( +
    + +
    + ); + }, + + _renderCryptoInfo: function() { + const client = MatrixClientPeg.get(); + const deviceId = client.deviceId; + let identityKey = client.getDeviceEd25519Key(); + if (!identityKey) { + identityKey = _t(""); + } else { + identityKey = FormattingUtils.formatCryptoKey(identityKey); + } + + let importExportButtons = null; + + if (client.isCryptoEnabled) { + importExportButtons = ( +
    + + { _t("Export E2E room keys") } + + + { _t("Import E2E room keys") } + +
    + ); + } + return ( +
    +

    { _t("Cryptography") }

    +
    +
      +
    • + { deviceId }
    • +
    • + { identityKey }
    • +
    + { importExportButtons } +
    +
    + { CRYPTO_SETTINGS.map( this._renderDeviceSetting ) } +
    +
    + ); + }, + + _renderIgnoredUsers: function() { + if (this.state.ignoredUsers.length > 0) { + const updateHandler = this._refreshIgnoredUsers; + return ( +
    +

    { _t("Ignored Users") }

    +
    +
      + { this.state.ignoredUsers.map(function(userId) { + return (); + }) } +
    +
    +
    + ); + } else return (
    ); + }, + + _renderDeviceSetting: function(setting) { + const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); + return ( +
    + +
    + ); + }, + + _renderDevicesPanel: function() { + const DevicesPanel = sdk.getComponent('settings.DevicesPanel'); + return ( +
    +

    { _t("Devices") }

    + +
    + ); + }, + + _renderBugReport: function() { + if (!SdkConfig.get().bug_report_endpoint_url) { + return
    ; + } + return ( +
    +

    { _t("Submit Debug Logs") }

    +
    +

    { + _t( "If you've submitted a bug via GitHub, debug logs can help " + + "us track down the problem. Debug logs contain application " + + "usage data including your username, the IDs or aliases of " + + "the rooms or groups you have visited and the usernames of " + + "other users. They do not contain messages.", + ) + }

    + +
    +
    + ); + }, + + _renderAnalyticsControl: function() { + if (!SdkConfig.get().piwik) return
    ; + + return
    +

    { _t('Analytics') }

    +
    + { _t('Riot collects anonymous analytics to allow us to improve the application.') } +
    + { _t('Privacy is important to us, so we don\'t collect any personal' + + ' or identifiable data for our analytics.') } + + { _t('Learn more about how we use analytics.') } + + { ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) } +
    +
    ; + }, + + _renderLabs: function() { + const features = []; + SettingsStore.getLabsFeatures().forEach((featureId) => { + // TODO: this ought to be a separate component so that we don't need + // to rebind the onChange each time we render + const onChange = async(e) => { + const checked = e.target.checked; + if (featureId === "feature_lazyloading") { + const confirmed = await this._onLazyLoadChanging(checked); + if (!confirmed) { + e.preventDefault(); + return; + } + } + await SettingsStore.setFeatureEnabled(featureId, checked); + this.forceUpdate(); + }; + + features.push( +
    + + +
    ); + }); + + // No labs section when there are no features in labs + if (features.length === 0) { + return null; + } + + return ( +
    +

    { _t("Labs") }

    +
    +

    { _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.

    + { features } +
    +
    + ); + }, + + _onLazyLoadChanging: async function(enabling) { + // don't prevent turning LL off when not supported + if (enabling) { + const supported = await MatrixClientPeg.get().doesServerSupportLazyLoading(); + if (!supported) { + await new Promise((resolve) => { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createDialog(QuestionDialog, { + title: _t("Lazy loading members not supported"), + description: +
    + { _t("Lazy loading is not supported by your " + + "current homeserver.") } +
    , + button: _t("OK"), + onFinished: resolve, + }); + }); + return false; + } + } + return true; + }, + + _renderDeactivateAccount: function() { + return
    +

    { _t("Deactivate Account") }

    +
    + { _t("Deactivate my account") } + +
    +
    ; + }, + + _renderTermsAndConditionsLinks: function() { + if (SdkConfig.get().terms_and_conditions_links) { + const tncLinks = []; + for (const tncEntry of SdkConfig.get().terms_and_conditions_links) { + tncLinks.push(); + } + return
    +

    { _t("Legal") }

    +
    + {tncLinks} +
    +
    ; + } else { + return null; + } + }, + + _renderClearCache: function() { + return
    +

    { _t("Clear Cache") }

    +
    + + { _t("Clear Cache and Reload") } + +
    +
    ; + }, + + _renderCheckUpdate: function() { + const platform = PlatformPeg.get(); + if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) { + return
    +

    { _t('Updates') }

    +
    + + { _t('Check for update') } + +
    +
    ; + } + return
    ; + }, + + _renderBulkOptions: function() { + const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => { + return r.hasMembershipState(this._me, "invite"); + }); + if (invitedRooms.length === 0) { + return null; + } + + const Spinner = sdk.getComponent("elements.Spinner"); + + let reject = ; + if (!this.state.rejectingInvites) { + // bind() the invited rooms so any new invites that may come in as this button is clicked + // don't inadvertently get rejected as well. + const onClick = this._onRejectAllInvitesClicked.bind(this, invitedRooms); + reject = ( + + { _t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length}) } + + ); + } + + return
    +

    { _t("Bulk Options") }

    +
    + { reject } +
    +
    ; + }, + + _renderElectronSettings: function() { + const settings = this.state.electron_settings; + if (!settings) return; + + // TODO: This should probably be a granular setting, but it only applies to electron + // and ends up being get/set outside of matrix anyways (local system setting). + return
    +

    { _t('Desktop specific') }

    +
    +
    + + +
    +
    +
    ; + }, + + _onAutoLaunchChanged: function(e) { + const {ipcRenderer} = require('electron'); + ipcRenderer.send('settings_set', 'auto-launch', e.target.checked); + }, + + _mapWebRtcDevicesToSpans: function(devices) { + return devices.map((device) => { device.label }); + }, + + _setAudioOutput: function(deviceId) { + this.setState({activeAudioOutput: deviceId}); + CallMediaHandler.setAudioOutput(deviceId); + }, + + _setAudioInput: function(deviceId) { + this.setState({activeAudioInput: deviceId}); + CallMediaHandler.setAudioInput(deviceId); + }, + + _setVideoInput: function(deviceId) { + this.setState({activeVideoInput: deviceId}); + CallMediaHandler.setVideoInput(deviceId); + }, + + _requestMediaPermissions: function(event) { + 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'), + }); + }, + ]); + } + }, + + _renderWebRtcDeviceSettings: function() { + if (this.state.mediaDevices === false) { + return ( + + { _t('Missing Media Permissions, click here to request.') } + + ); + } else if (!this.state.mediaDevices) return; + + const Dropdown = sdk.getComponent('elements.Dropdown'); + + let speakerDropdown =

    { _t('No Audio Outputs detected') }

    ; + let microphoneDropdown =

    { _t('No Microphones detected') }

    ; + let webcamDropdown =

    { _t('No Webcams detected') }

    ; + + const defaultOption = { + deviceId: '', + label: _t('Default Device'), + }; + + const audioOutputs = this.state.mediaDevices.audiooutput.slice(0); + if (audioOutputs.length > 0) { + let defaultOutput = ''; + if (!audioOutputs.some((input) => input.deviceId === 'default')) { + audioOutputs.unshift(defaultOption); + } else { + defaultOutput = 'default'; + } + + speakerDropdown =
    +

    { _t('Audio Output') }

    + + { this._mapWebRtcDevicesToSpans(audioOutputs) } + +
    ; + } + + const audioInputs = this.state.mediaDevices.audioinput.slice(0); + if (audioInputs.length > 0) { + let defaultInput = ''; + if (!audioInputs.some((input) => input.deviceId === 'default')) { + audioInputs.unshift(defaultOption); + } else { + defaultInput = 'default'; + } + + microphoneDropdown =
    +

    { _t('Microphone') }

    + + { this._mapWebRtcDevicesToSpans(audioInputs) } + +
    ; + } + + const videoInputs = this.state.mediaDevices.videoinput.slice(0); + if (videoInputs.length > 0) { + let defaultInput = ''; + if (!videoInputs.some((input) => input.deviceId === 'default')) { + videoInputs.unshift(defaultOption); + } else { + defaultInput = 'default'; + } + + webcamDropdown =
    +

    { _t('Camera') }

    + + { this._mapWebRtcDevicesToSpans(videoInputs) } + +
    ; + } + + return
    + { speakerDropdown } + { microphoneDropdown } + { webcamDropdown } +
    ; + }, + + _renderWebRtcSettings: function() { + return
    +

    { _t('VoIP') }

    +
    + { WEBRTC_SETTINGS.map(this._renderDeviceSetting) } + { PlatformPeg.get().isElectron() && this._renderPushToTalkSettings() } + { this._renderWebRtcDeviceSettings() } +
    +
    ; + }, + + onSelfShareClick: function() { + const cli = MatrixClientPeg.get(); + const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); + Modal.createTrackedDialog('share self dialog', '', ShareDialog, { + target: cli.getUser(this._me), + }); + }, + + _showSpoiler: function(event) { + const target = event.target; + target.innerHTML = target.getAttribute('data-spoiler'); + + const range = document.createRange(); + range.selectNodeContents(target); + + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }, + + nameForMedium: function(medium) { + if (medium === 'msisdn') return _t('Phone'); + if (medium === 'email') return _t('Email'); + return medium[0].toUpperCase() + medium.slice(1); + }, + + presentableTextForThreepid: function(threepid) { + if (threepid.medium === 'msisdn') { + return '+' + threepid.address; + } else { + return threepid.address; + } + }, + + render: function() { + const Loader = sdk.getComponent("elements.Spinner"); + switch (this.state.phase) { + case "UserSettings.LOADING": + return ( + + ); + case "UserSettings.DISPLAY": + break; // quit the switch to return the common state + default: + throw new Error("Unknown state.phase => " + this.state.phase); + } + // can only get here if phase is UserSettings.DISPLAY + const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); + const ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName"); + const ChangePassword = sdk.getComponent("views.settings.ChangePassword"); + const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); + const Notifications = sdk.getComponent("settings.Notifications"); + const EditableText = sdk.getComponent('elements.EditableText'); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); + + const avatarUrl = ( + this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null + ); + + const threepidsSection = this.state.threepids.map((val, pidIndex) => { + const id = "3pid-" + val.address; + // TODO: make a separate component to avoid having to rebind onClick + // each time we render + const onRemoveClick = (e) => this.onRemoveThreepidClicked(val); + return ( +
    +
    + +
    +
    + +
    +
    + +
    +
    + ); + }); + let addEmailSection; + if (this.state.email_add_pending) { + addEmailSection = ; + } else { + addEmailSection = ( +
    +
    + +
    +
    + +
    +
    + +
    +
    + ); + } + const AddPhoneNumber = sdk.getComponent('views.settings.AddPhoneNumber'); + const addMsisdnSection = ( + + ); + threepidsSection.push(addEmailSection); + threepidsSection.push(addMsisdnSection); + + const accountJsx = ( + + ); + + let notificationArea; + if (this.state.threepids !== undefined) { + notificationArea = (
    +

    { _t("Notifications") }

    + +
    + +
    +
    ); + } + + const olmVersion = MatrixClientPeg.get().olmVersion; + // If the olmVersion is not defined then either crypto is disabled, or + // we are using a version old version of olm. We assume the former. + let olmVersionString = ""; + if (olmVersion) { + olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`; + } + + return ( +
    + + + + +

    { _t("Profile") }

    + +
    +
    +
    +
    + +
    +
    + +
    +
    + { threepidsSection } +
    + +
    + + {_t("Remove + +
    + +
    +
    + + +
    +
    +
    + +

    { _t("Account") }

    + +
    + + { _t("Sign out") } + + { this.state.userHasGeneratedPassword ? +
    + { _t("To return to your account in future you need to set a password") } +
    : null + } + + { accountJsx } +
    + + { this._renderGroupSettings() } + + { this._renderReferral() } + + { notificationArea } + + { this._renderUserInterfaceSettings() } + { this._renderLabs() } + { this._renderWebRtcSettings() } + { this._renderDevicesPanel() } + { this._renderCryptoInfo() } + { this._renderIgnoredUsers() } + { this._renderBulkOptions() } + { this._renderBugReport() } + + { PlatformPeg.get().isElectron() && this._renderElectronSettings() } + + { this._renderAnalyticsControl() } + +

    { _t("Advanced") }

    + +
    +
    + { _t("Logged in as:") + ' ' } + + { this._me } + +
    +
    + { _t('Access Token:') + ' ' } + + <{ _t("click to reveal") }> + +
    +
    + { _t("Homeserver is") } { MatrixClientPeg.get().getHomeserverUrl() } +
    +
    + { _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() } +
    +
    + { _t('matrix-react-sdk version:') } { (REACT_SDK_VERSION !== '') + ? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) + : REACT_SDK_VERSION + }
    + { _t('riot-web version:') } { (this.state.vectorVersion !== undefined) + ? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion) + : 'unknown' + }
    + { _t("olm version:") } { olmVersionString }
    +
    +
    + + { this._renderCheckUpdate() } + + { this._renderClearCache() } + + { this._renderDeactivateAccount() } + + { this._renderTermsAndConditionsLinks() } + +
    +
    + ); + }, +}); diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index d23d6488b62..2ab2bd92f3a 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -17,8 +17,10 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; - import ResizeObserver from 'resize-observer-polyfill'; +import SettingsStore from '../../../settings/SettingsStore'; + +import * as PushToTalk from '../../../PushToTalk'; import dis from '../../../dispatcher'; @@ -112,6 +114,13 @@ export default class PersistedElement extends React.Component { } componentDidMount() { + // Start Push-To-Talk service when Jitsi widget is mounted + // TODO: This seems quite hacky - is there a better way to + // check if this is a Jitsi vs. StickerPicker widget? + if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue('pushToTalk').enabled) { + PushToTalk.enable(SettingsStore.getValue('pushToTalk').keybinding); + } + this.updateChild(); } @@ -124,6 +133,11 @@ export default class PersistedElement extends React.Component { this.resizeObserver.disconnect(); window.removeEventListener('resize', this._repositionChild); dis.unregister(this._dispatcherRef); + + // Stop Push-To-Talk service when Jitsi widget is unmounted + if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue('pushToTalk').enabled) { + PushToTalk.disable(); + } } _onAction(payload) { @@ -168,4 +182,5 @@ export default class PersistedElement extends React.Component { return
    ; } + } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index cf68fed8ba2..3841049fb9f 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -89,7 +89,7 @@ export const SETTINGS = { // }, "feature_pinning": { isFeature: true, - displayName: _td("Message Pinning"), + displayName: _td('Message Pinning'), supportedLevels: LEVELS_FEATURE, default: false, }, @@ -100,6 +100,15 @@ export const SETTINGS = { default: false, controller: new CustomStatusController(), }, + "pushToTalk": { + displayName: _td('Push-to-Talk'), + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + default: { + enabled: false, + keybinding: [], + ascii: 'Not set', + }, + }, "feature_room_breadcrumbs": { isFeature: true, displayName: _td("Show recent room avatars above the room list (refresh to apply changes)"), @@ -286,21 +295,21 @@ export const SETTINGS = { supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM, displayName: { "default": _td('Enable inline URL previews by default'), - "room-account": _td("Enable URL previews for this room (only affects you)"), - "room": _td("Enable URL previews by default for participants in this room"), + "room-account": _td('Enable URL previews for this room (only affects you)'), + "room": _td('Enable URL previews by default for participants in this room'), }, default: true, }, "urlPreviewsEnabled_e2ee": { supportedLevels: ['room-device', 'room-account'], displayName: { - "room-account": _td("Enable URL previews for this room (only affects you)"), + "room-account": _td('Enable URL previews for this room (only affects you)'), }, default: false, }, "roomColor": { supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM, - displayName: _td("Room Colour"), + displayName: _td('Room Colour'), default: { primary_color: null, // Hex string, eg: #000000 secondary_color: null, // Hex string, eg: #000000 From 1a2923b5f0df1e019fa1ab3b70a8fc2f85946c27 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 15 Nov 2018 21:01:26 +0100 Subject: [PATCH 02/25] lint --- src/WidgetMessaging.js | 1 - src/components/structures/UserSettings.js | 2 -- src/components/views/elements/PersistedElement.js | 1 - 3 files changed, 4 deletions(-) diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index b211b96cddb..f8c6961f9d8 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -35,7 +35,6 @@ const OUTBOUND_API_NAME = 'toWidget'; export default class WidgetMessaging { constructor(widgetId, widgetUrl, target) { - console.log("I'm alive! My URL is:", widgetUrl) this.widgetId = widgetId; this.widgetUrl = widgetUrl; this.target = target; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 6375bfae4c7..2c3bdeec8b7 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -685,7 +685,6 @@ module.exports = React.createClass({ // Used for displaying ascii-representation of current keys // in the UI listenKeydown = function(event) { - // TODO: Show RightShift and things const key = self._translateKeybinding(event.code); const index = keyAscii.indexOf(key); if (index === -1) { @@ -804,7 +803,6 @@ module.exports = React.createClass({ PushToTalk.enable(currentPTTState.keybinding); } else { // Disable push to talk - console.log("Disabling push to talk...") this.setState({pushToTalkEnabled: false}); currentPTTState.enabled = false; diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index 2ab2bd92f3a..1f1b17e98eb 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -182,5 +182,4 @@ export default class PersistedElement extends React.Component { return
    ; } - } From f5fbf97bf3b190b9da7800f9395aa30be58d0956 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 21 Nov 2018 11:58:55 +0100 Subject: [PATCH 03/25] Fix settings look --- src/components/structures/UserSettings.js | 42 ++++++++++------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 2c3bdeec8b7..c0461038742 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -815,33 +815,27 @@ module.exports = React.createClass({ _renderPushToTalkSettings: function() { const id = "pushToTalk"; - const buttonLabel = this.state.settingKeybinding ? 'Stop' : 'Set'; + const buttonLabel = this.state.settingKeybinding ? _t('Stop') : _t('Set'); const activated = SettingsStore.getValueAt(SettingLevel.DEVICE, id).enabled; return ( -
    - - - - - - - - -
    - - - {"Shortcut: " + this.state.pushToTalkAscii} - -
    +
    + + + + + {" | Shortcut: " + this.state.pushToTalkAscii + " "} + + +
    ); }, From 4a0a26a376f080e868547b8f1cb2332d60f39e63 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 12 Feb 2019 19:27:16 +0000 Subject: [PATCH 04/25] Add support for custom keybinding names --- src/components/structures/UserSettings.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index c0461038742..45efa728342 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -673,6 +673,15 @@ module.exports = React.createClass({ key = key.substring(3); } + const replacements = { + 'ShiftLeft': 'Shift', + }; + + // Custom redefinitions of key names + if (replacements[key]) { + key = replacements[key]; + } + return key; }, From aaa8aaa7abbfd897812668134cbe37f7877b2499 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 12 Feb 2019 19:27:30 +0000 Subject: [PATCH 05/25] Update rimraf --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fba17bb9c9b..b0b22369d01 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "mocha": "^5.0.5", "react-addons-test-utils": "^15.4.0", "require-json": "0.0.1", - "rimraf": "^2.4.3", + "rimraf": "^2.6.2", "sinon": "^5.0.7", "source-map-loader": "^0.2.3", "walk": "^2.3.9", From dc0a6aff02eef2da18fbb9c2847377607a7a5219 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 21 Feb 2019 20:12:55 +0000 Subject: [PATCH 06/25] Remove UserSettings.js --- package.json | 1 + src/components/structures/UserSettings.js | 1633 ----------------- .../views/settings/tabs/VoiceSettingsTab.js | 231 ++- 3 files changed, 231 insertions(+), 1634 deletions(-) delete mode 100644 src/components/structures/UserSettings.js diff --git a/package.json b/package.json index b0b22369d01..3db56306c36 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "classnames": "^2.1.2", "commonmark": "^0.28.1", "counterpart": "^0.18.0", + "electron": "^4.0.4", "emojione": "2.2.7", "file-saver": "^1.3.3", "filesize": "3.5.6", diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js deleted file mode 100644 index 45efa728342..00000000000 --- a/src/components/structures/UserSettings.js +++ /dev/null @@ -1,1633 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2017, 2018 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 SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; - -const React = require('react'); -const ReactDOM = require('react-dom'); -import PropTypes from 'prop-types'; -const sdk = require('../../index'); -const MatrixClientPeg = require("../../MatrixClientPeg"); -const PlatformPeg = require("../../PlatformPeg"); -const Modal = require('../../Modal'); -const dis = require("../../dispatcher"); -import sessionStore from '../../stores/SessionStore'; -import Promise from 'bluebird'; -const packageJson = require('../../../package.json'); -const UserSettingsStore = require('../../UserSettingsStore'); -const CallMediaHandler = require('../../CallMediaHandler'); -const Email = require('../../email'); -const AddThreepid = require('../../AddThreepid'); -const SdkConfig = require('../../SdkConfig'); -import Analytics from '../../Analytics'; -import AccessibleButton from '../views/elements/AccessibleButton'; -import { _t, _td } from '../../languageHandler'; -import * as languageHandler from '../../languageHandler'; -import * as FormattingUtils from '../../utils/FormattingUtils'; -import * as PushToTalk from '../../PushToTalk'; - -// if this looks like a release, use the 'version' from package.json; else use -// the git sha. Prepend version with v, to look like riot-web version -const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || ''; - -// Simple method to help prettify GH Release Tags and Commit Hashes. -const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i; -const gHVersionLabel = function(repo, token='') { - const match = token.match(semVerRegex); - let url; - if (match && match[1]) { // basic semVer string possibly with commit hash - url = (match.length > 1 && match[2]) - ? `https://github.com/${repo}/commit/${match[2]}` - : `https://github.com/${repo}/releases/tag/v${match[1]}`; - } else { - url = `https://github.com/${repo}/commit/${token.split('-')[0]}`; - } - return { token }; -}; - -// Enumerate some simple 'flip a bit' UI settings (if any). The strings provided here -// must be settings defined in SettingsStore. -const SIMPLE_SETTINGS = [ - { id: "urlPreviewsEnabled" }, - { id: "autoplayGifsAndVideos" }, - { id: "alwaysShowEncryptionIcons" }, - { id: "hideReadReceipts" }, - { id: "dontSendTypingNotifications" }, - { id: "alwaysShowTimestamps" }, - { id: "showTwelveHourTimestamps" }, - { id: "hideJoinLeaves" }, - { id: "hideAvatarChanges" }, - { id: "hideDisplaynameChanges" }, - { id: "useCompactLayout" }, - { id: "hideRedactions" }, - { id: "enableSyntaxHighlightLanguageDetection" }, - { id: "MessageComposerInput.autoReplaceEmoji" }, - { id: "MessageComposerInput.dontSuggestEmoji" }, - { id: "Pill.shouldHidePillAvatar" }, - { id: "TextualBody.disableBigEmoji" }, - { id: "VideoView.flipVideoHorizontally" }, - { id: "TagPanel.disableTagPanel" }, - { id: "enableWidgetScreenshots" }, - { id: "RoomSubList.showEmpty" }, - { id: "pinMentionedRooms" }, - { id: "pinUnreadRooms" }, - { id: "showDeveloperTools" }, -]; - -// These settings must be defined in SettingsStore -const ANALYTICS_SETTINGS = [ - { - id: 'analyticsOptIn', - fn: function(checked) { - checked ? Analytics.enable() : Analytics.disable(); - }, - }, -]; - -// These settings must be defined in SettingsStore -const WEBRTC_SETTINGS = [ - { - id: 'webRtcForceTURN', - fn: (val) => { - MatrixClientPeg.get().setForceTURN(val); - }, - }, -]; - -// These settings must be defined in SettingsStore -const CRYPTO_SETTINGS = [ - { - id: 'blacklistUnverifiedDevices', - fn: function(checked) { - MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); - }, - }, -]; - -// Enumerate the available themes, with a nice human text label. -// 'label' is how we describe it in the UI. -// 'value' is the value for the theme setting -// -// XXX: Ideally we would have a theme manifest or something and they'd be nicely -// packaged up in a single directory, and/or located at the application layer. -// But for now for expedience we just hardcode them here. -const THEMES = [ - { label: _td('Light theme'), value: 'light' }, - { label: _td('Dark theme'), value: 'dark' }, - { label: _td('Status.im theme'), value: 'status' }, -]; - -const IgnoredUser = React.createClass({ - propTypes: { - userId: PropTypes.string.isRequired, - onUnignored: PropTypes.func.isRequired, - }, - - _onUnignoreClick: function() { - const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers(); - const index = ignoredUsers.indexOf(this.props.userId); - if (index !== -1) { - ignoredUsers.splice(index, 1); - MatrixClientPeg.get().setIgnoredUsers(ignoredUsers) - .then(() => this.props.onUnignored(this.props.userId)); - } else this.props.onUnignored(this.props.userId); - }, - - render: function() { - return ( -
  • - - { _t("Unignore") } - - { this.props.userId } -
  • - ); - }, -}); - -let listenKeydown; -let listenKeyup; - -module.exports = React.createClass({ - displayName: 'UserSettings', - - propTypes: { - onClose: PropTypes.func, - // The brand string given when creating email pushers - brand: PropTypes.string, - - // The base URL to use in the referral link. Defaults to window.location.origin. - referralBaseUrl: PropTypes.string, - - // Team token for the referral link. If falsy, the referral section will - // not appear - teamToken: PropTypes.string, - }, - - getDefaultProps: function() { - return { - onClose: function() {}, - }; - }, - - getInitialState: function() { - return { - avatarUrl: null, - threepids: [], - phase: "UserSettings.LOADING", // LOADING, DISPLAY - email_add_pending: false, - vectorVersion: undefined, - rejectingInvites: false, - mediaDevices: null, - ignoredUsers: [], - pushToTalkAscii: SettingsStore.getValue('pushToTalk').ascii, - pushToTalkKeybinding: SettingsStore.getValue('pushToTalk').keybinding, - pushToTalkEnabled: SettingsStore.getValue('pushToTalk').enabled, - }; - }, - - componentWillMount: function() { - this._unmounted = false; - this._addThreepid = null; - - if (PlatformPeg.get()) { - Promise.resolve().then(() => { - return PlatformPeg.get().getAppVersion(); - }).done((appVersion) => { - if (this._unmounted) return; - this.setState({ - vectorVersion: appVersion, - }); - }, (e) => { - console.log("Failed to fetch app version", e); - }); - } - - this._refreshMediaDevices(); - this._refreshIgnoredUsers(); - - // Bulk rejecting invites: - // /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms() - // will still return rooms with invites. To get around this, add a listener for - // membership updates and kick the UI. - MatrixClientPeg.get().on("RoomMember.membership", this._onInviteStateChange); - - dis.dispatch({ - action: 'panel_disable', - sideDisabled: true, - middleDisabled: true, - }); - this._refreshFromServer(); - - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - - ipcRenderer.on('settings', this._electronSettings); - ipcRenderer.send('settings_get'); - } - - this.setState({ - language: languageHandler.getCurrentLanguage(), - }); - - this._sessionStore = sessionStore; - this._sessionStoreToken = this._sessionStore.addListener( - this._setStateFromSessionStore, - ); - this._setStateFromSessionStore(); - }, - - componentDidMount: function() { - this.dispatcherRef = dis.register(this.onAction); - this._me = MatrixClientPeg.get().credentials.userId; - }, - - componentWillUnmount: function() { - this._unmounted = true; - dis.dispatch({ - action: 'panel_disable', - sideDisabled: false, - middleDisabled: false, - }); - dis.unregister(this.dispatcherRef); - const cli = MatrixClientPeg.get(); - if (cli) { - cli.removeListener("RoomMember.membership", this._onInviteStateChange); - } - - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - ipcRenderer.removeListener('settings', this._electronSettings); - - // Stop recording push-to-talk shortcut if Settings window is closed - this._stopRecordingGlobalShortcut(); - } - }, - - // `UserSettings` assumes that the client peg will not be null, so give it some - // sort of assurance here by only allowing a re-render if the client is truthy. - // - // This is required because `UserSettings` maintains its own state and if this state - // updates (e.g. during _setStateFromSessionStore) after the client peg has been made - // null (during logout), then it will attempt to re-render and throw errors. - shouldComponentUpdate: function() { - return Boolean(MatrixClientPeg.get()); - }, - - _setStateFromSessionStore: function() { - this.setState({ - userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), - }); - }, - - _electronSettings: function(ev, settings) { - this.setState({ electron_settings: settings }); - }, - - _refreshMediaDevices: function(stream) { - if (stream) { - // kill stream so that we don't leave it lingering around with webcam enabled etc - // as here we called gUM to ask user for permission to their device names only - stream.getTracks().forEach((track) => track.stop()); - } - - Promise.resolve().then(() => { - return CallMediaHandler.getDevices(); - }).then((mediaDevices) => { - // console.log("got mediaDevices", mediaDevices, this._unmounted); - if (this._unmounted) return; - this.setState({ - mediaDevices, - activeAudioOutput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audiooutput'), - activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'), - activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'), - }); - }); - }, - - _refreshFromServer: function() { - const self = this; - Promise.all([ - UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(), - ]).done(function(resps) { - self.setState({ - avatarUrl: resps[0].avatar_url, - threepids: resps[1].threepids, - phase: "UserSettings.DISPLAY", - }); - }, function(error) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to load user settings: " + error); - Modal.createTrackedDialog('Can\'t load user settings', '', ErrorDialog, { - title: _t("Can't load user settings"), - description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")), - }); - }); - }, - - _refreshIgnoredUsers: function(userIdUnignored=null) { - const users = MatrixClientPeg.get().getIgnoredUsers(); - if (userIdUnignored) { - const index = users.indexOf(userIdUnignored); - if (index !== -1) users.splice(index, 1); - } - this.setState({ - ignoredUsers: users, - }); - }, - - onAction: function(payload) { - if (payload.action === "notifier_enabled") { - this.forceUpdate(); - } else if (payload.action === "ignore_state_changed") { - this._refreshIgnoredUsers(); - } - }, - - onAvatarPickerClick: function(ev) { - if (this.refs.file_label) { - this.refs.file_label.click(); - } - }, - - onAvatarSelected: function(ev) { - const self = this; - const changeAvatar = this.refs.changeAvatar; - if (!changeAvatar) { - console.error("No ChangeAvatar found to upload image to!"); - return; - } - changeAvatar.onFileSelected(ev).done(function() { - // dunno if the avatar changed, re-check it. - self._refreshFromServer(); - }, function(err) { - // const errMsg = (typeof err === "string") ? err : (err.error || ""); - console.error("Failed to set avatar: " + err); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, { - title: _t("Failed to set avatar."), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - }); - }, - - onAvatarRemoveClick: function() { - MatrixClientPeg.get().setAvatarUrl(null); - this.setState({avatarUrl: null}); // the avatar update will complete async for us - }, - - onLogoutClicked: function(ev) { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, { - title: _t("Sign out"), - description: -
    - { _t("For security, logging out will delete any end-to-end " + - "encryption keys from this browser. If you want to be able " + - "to decrypt your conversation history from future Riot sessions, " + - "please export your room keys for safe-keeping.") } -
    , - button: _t("Sign out"), - extraButtons: [ - , - ], - onFinished: (confirmed) => { - if (confirmed) { - dis.dispatch({action: 'logout'}); - if (this.props.onFinished) { - this.props.onFinished(); - } - } - }, - }); - }, - - onPasswordChangeError: function(err) { - let errMsg = err.error || ""; - if (err.httpStatus === 403) { - errMsg = _t("Failed to change password. Is your password correct?"); - } else if (err.httpStatus) { - errMsg += ` (HTTP status ${err.httpStatus})`; - } - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to change password: " + errMsg); - Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, { - title: _t("Error"), - description: errMsg, - }); - }, - - onPasswordChanged: function() { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Password changed', '', ErrorDialog, { - title: _t("Success"), - description: _t( - "Your password was successfully changed. You will not receive " + - "push notifications on other devices until you log back in to them", - ) + ".", - }); - }, - - _onAddEmailEditFinished: function(value, shouldSubmit) { - if (!shouldSubmit) return; - this._addEmail(); - }, - - _addEmail: function() { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - - const emailAddress = this.refs.add_email_input.value; - if (!Email.looksValid(emailAddress)) { - Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, { - title: _t("Invalid Email Address"), - description: _t("This doesn't appear to be a valid email address"), - }); - return; - } - this._addThreepid = new AddThreepid(); - // we always bind emails when registering, so let's do the - // same here. - this._addThreepid.addEmailAddress(emailAddress, true).done(() => { - Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { - title: _t("Verification Pending"), - description: _t( - "Please check your email and click on the link it contains. Once this " + - "is done, click continue.", - ), - button: _t('Continue'), - onFinished: this.onEmailDialogFinished, - }); - }, (err) => { - this.setState({email_add_pending: false}); - console.error("Unable to add email address " + emailAddress + " " + err); - Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, { - title: _t("Unable to add email address"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - }); - ReactDOM.findDOMNode(this.refs.add_email_input).blur(); - this.setState({email_add_pending: true}); - }, - - onRemoveThreepidClicked: function(threepid) { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Remove 3pid', '', QuestionDialog, { - title: _t("Remove Contact Information?"), - description: _t("Remove %(threePid)s?", { threePid: threepid.address }), - button: _t('Remove'), - onFinished: (submit) => { - if (submit) { - this.setState({ - phase: "UserSettings.LOADING", - }); - MatrixClientPeg.get().deleteThreePid(threepid.medium, threepid.address).then(() => { - return this._refreshFromServer(); - }).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Unable to remove contact information: " + err); - Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, { - title: _t("Unable to remove contact information"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - }).done(); - } - }, - }); - }, - - onEmailDialogFinished: function(ok) { - if (ok) { - this.verifyEmailAddress(); - } else { - this.setState({email_add_pending: false}); - } - }, - - verifyEmailAddress: function() { - this._addThreepid.checkEmailLinkClicked().done(() => { - this._addThreepid = null; - this.setState({ - phase: "UserSettings.LOADING", - }); - this._refreshFromServer(); - this.setState({email_add_pending: false}); - }, (err) => { - this.setState({email_add_pending: false}); - if (err.errcode == 'M_THREEPID_AUTH_FAILED') { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - const message = _t("Unable to verify email address.") + " " + - _t("Please check your email and click on the link it contains. Once this is done, click continue."); - Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { - title: _t("Verification Pending"), - description: message, - button: _t('Continue'), - onFinished: this.onEmailDialogFinished, - }); - } else { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Unable to verify email address: " + err); - Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, { - title: _t("Unable to verify email address."), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - } - }); - }, - - _onDeactivateAccountClicked: function() { - const DeactivateAccountDialog = sdk.getComponent("dialogs.DeactivateAccountDialog"); - Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {}); - }, - - _onBugReportClicked: function() { - const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); - if (!BugReportDialog) { - return; - } - Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); - }, - - _onClearCacheClicked: function() { - if (!PlatformPeg.get()) return; - - MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().store.deleteAllData().done(() => { - PlatformPeg.get().reload(); - }); - }, - - _onInviteStateChange: function(event, member, oldMembership) { - if (member.userId === this._me && oldMembership === "invite") { - this.forceUpdate(); - } - }, - - _onRejectAllInvitesClicked: function(rooms, ev) { - this.setState({ - rejectingInvites: true, - }); - // reject the invites - const promises = rooms.map((room) => { - return MatrixClientPeg.get().leave(room.roomId).catch((e) => { - // purposefully drop errors to the floor: we'll just have a non-zero number on the UI - // after trying to reject all the invites. - }); - }); - Promise.all(promises).then(() => { - this.setState({ - rejectingInvites: false, - }); - }); - }, - - _onExportE2eKeysClicked: function() { - Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => { - require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { - cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog')); - }, "e2e-export"); - }, { - matrixClient: MatrixClientPeg.get(), - }); - }, - - _onImportE2eKeysClicked: function() { - Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => { - require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => { - cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog')); - }, "e2e-export"); - }, { - matrixClient: MatrixClientPeg.get(), - }); - }, - - _renderGroupSettings: function() { - const GroupUserSettings = sdk.getComponent('groups.GroupUserSettings'); - return ; - }, - - _renderReferral: function() { - const teamToken = this.props.teamToken; - if (!teamToken) { - return null; - } - if (typeof teamToken !== 'string') { - console.warn('Team token not a string'); - return null; - } - const href = (this.props.referralBaseUrl || window.location.origin) + - `/#/register?referrer=${this._me}&team_token=${teamToken}`; - return ( -
    -

    Referral

    -
    - { _t("Refer a friend to Riot:") } { href } -
    -
    - ); - }, - - onLanguageChange: function(newLang) { - if (this.state.language !== newLang) { - SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); - this.setState({ - language: newLang, - }); - PlatformPeg.get().reload(); - } - }, - - _renderLanguageSetting: function() { - const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); - return
    - - -
    ; - }, - - _translateKeybinding(key) { - // Custom translations to make keycodes look nicer - - // KeyA -> A - if (key.startsWith('Key')) { - key = key.substring(3); - } - - const replacements = { - 'ShiftLeft': 'Shift', - }; - - // Custom redefinitions of key names - if (replacements[key]) { - key = replacements[key]; - } - - return key; - }, - - _startRecordingGlobalShortcut(asciiStateKey, codeStateKey) { - const keyAscii = []; - const keyCodes = []; - const self = this; - - // Record keypresses using KeyboardEvent - // Used for displaying ascii-representation of current keys - // in the UI - listenKeydown = function(event) { - const key = self._translateKeybinding(event.code); - const index = keyAscii.indexOf(key); - if (index === -1) { - keyAscii.push(key); - self.setState({pushToTalkAscii: keyAscii.join(' + ')}); - } - event.preventDefault(); - }; - listenKeyup = function(event) { - const index = keyAscii.indexOf(self._translateKeybinding(event.code)); - if (index !== -1) { - keyAscii.splice(index, 1); - } - event.preventDefault(); - }; - - window.addEventListener("keydown", listenKeydown); - window.addEventListener("keyup", listenKeyup); - - // Record keypresses using iohook - // Used for getting keycode-representation of current keys - // for later global shortcut registration - const {ipcRenderer} = require('electron'); - ipcRenderer.send('start-listening-keys'); - - // When a key is pressed, add all current pressed keys to the shortcut - // When a key is lifted, don't remove it from the shortcut - - // This enables a nicer shortcut-recording experience, as the user can - // press down their desired keys, release them, and then save the - // shortcut without all the keys disappearing - ipcRenderer.on('keypress', function(ev, event) { - if (event.keydown) { - const index = keyCodes.indexOf(event.keycode); - if (index === -1) { - keyCodes.push(event.keycode); - // slice is needed here to save a new copy of the keycodes - // array to the state, else if we update keycodes later, it - // still updates the state since the state has a ref to this array - self.setState({pushToTalkKeybinding: keyCodes.slice()}); - } - } else { - const index = keyCodes.indexOf(event.keycode); - if (index !== -1) { - keyCodes.splice(index, 1); - } - } - }); - - // Stop recording shortcut if window loses focus - ipcRenderer.on('window-blurred', () => { - if (this.state.settingKeybinding) { - this._stopRecordingGlobalShortcut(); - } - }); - }, - - _stopRecordingGlobalShortcut() { - // Stop recording KeyboardEvent keypresses - window.removeEventListener("keydown", listenKeydown); - window.removeEventListener("keyup", listenKeyup); - - // Stop recording iohook keypresses - const {ipcRenderer} = require('electron'); - ipcRenderer.send('stop-listening-keys'); - - this.setState({settingKeybinding: false}); - }, - - _onSetPushToTalkClicked: function() { - // Either record or save a new shortcut - const id = 'pushToTalk'; - const currentPTTState = SettingsStore.getValue(id); - - // Determine if we're reading shortcuts or setting them - if (!this.state.settingKeybinding) { - // Start listening for keypresses and show current - // held shortcut on screen - // Run some sort of function that just loops until the state changes back to - // not setting - this.state.pushToTalkAscii = 'Press Keys'; - this._startRecordingGlobalShortcut('pushToTalkAscii', 'pushToTalkKeybinding'); - } else { - this._stopRecordingGlobalShortcut(); - - // Disable and unregister old shortcut - PushToTalk.disable(); - - // Set the keybinding they've currently selected - currentPTTState.keybinding = this.state.pushToTalkKeybinding; - currentPTTState.ascii = this.state.pushToTalkAscii; - - // Update push to talk keybinding - SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); - - // Enable and register new shortcut - PushToTalk.enable(currentPTTState.keybinding); - } - - // Toggle setting state - this.setState({settingKeybinding: !this.state.settingKeybinding}); - }, - - _onTogglePushToTalkClicked: function(e) { - // Enable or disable push to talk functionality - const id = 'pushToTalk'; - const currentPTTState = SettingsStore.getValueAt(SettingLevel.DEVICE, id); - - if (e.target.checked) { - // Enable push to talk - - this.setState({pushToTalkEnabled: true}); - currentPTTState.enabled = true; - SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); - - PushToTalk.enable(currentPTTState.keybinding); - } else { - // Disable push to talk - - this.setState({pushToTalkEnabled: false}); - currentPTTState.enabled = false; - SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); - this.setState({pushToTalkKeybinding: []}); - - PushToTalk.disable(); - } - }, - - _renderPushToTalkSettings: function() { - const id = "pushToTalk"; - const buttonLabel = this.state.settingKeybinding ? _t('Stop') : _t('Set'); - const activated = SettingsStore.getValueAt(SettingLevel.DEVICE, id).enabled; - - return ( -
    - - - - - {" | Shortcut: " + this.state.pushToTalkAscii + " "} - - - -
    - ); - }, - - _renderUserInterfaceSettings: function() { - // TODO: this ought to be a separate component so that we don't need - // to rebind the onChange each time we render - const onChange = (e) => - SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); - return ( -
    -

    { _t("User Interface") }

    -
    - { SIMPLE_SETTINGS.map( this._renderAccountSetting ) } - { THEMES.map( this._renderThemeOption ) } - - - - - - - -
    { _t('Autocomplete Delay (ms):') } - -
    - { this._renderLanguageSetting() } -
    -
    - ); - }, - - _renderAccountSetting: function(setting) { - const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); - return ( -
    - -
    - ); - }, - - _renderThemeOption: function(setting) { - const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); - const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value}); - return ( -
    - -
    - ); - }, - - _renderCryptoInfo: function() { - const client = MatrixClientPeg.get(); - const deviceId = client.deviceId; - let identityKey = client.getDeviceEd25519Key(); - if (!identityKey) { - identityKey = _t(""); - } else { - identityKey = FormattingUtils.formatCryptoKey(identityKey); - } - - let importExportButtons = null; - - if (client.isCryptoEnabled) { - importExportButtons = ( -
    - - { _t("Export E2E room keys") } - - - { _t("Import E2E room keys") } - -
    - ); - } - return ( -
    -

    { _t("Cryptography") }

    -
    -
      -
    • - { deviceId }
    • -
    • - { identityKey }
    • -
    - { importExportButtons } -
    -
    - { CRYPTO_SETTINGS.map( this._renderDeviceSetting ) } -
    -
    - ); - }, - - _renderIgnoredUsers: function() { - if (this.state.ignoredUsers.length > 0) { - const updateHandler = this._refreshIgnoredUsers; - return ( -
    -

    { _t("Ignored Users") }

    -
    -
      - { this.state.ignoredUsers.map(function(userId) { - return (); - }) } -
    -
    -
    - ); - } else return (
    ); - }, - - _renderDeviceSetting: function(setting) { - const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); - return ( -
    - -
    - ); - }, - - _renderDevicesPanel: function() { - const DevicesPanel = sdk.getComponent('settings.DevicesPanel'); - return ( -
    -

    { _t("Devices") }

    - -
    - ); - }, - - _renderBugReport: function() { - if (!SdkConfig.get().bug_report_endpoint_url) { - return
    ; - } - return ( -
    -

    { _t("Submit Debug Logs") }

    -
    -

    { - _t( "If you've submitted a bug via GitHub, debug logs can help " + - "us track down the problem. Debug logs contain application " + - "usage data including your username, the IDs or aliases of " + - "the rooms or groups you have visited and the usernames of " + - "other users. They do not contain messages.", - ) - }

    - -
    -
    - ); - }, - - _renderAnalyticsControl: function() { - if (!SdkConfig.get().piwik) return
    ; - - return
    -

    { _t('Analytics') }

    -
    - { _t('Riot collects anonymous analytics to allow us to improve the application.') } -
    - { _t('Privacy is important to us, so we don\'t collect any personal' - + ' or identifiable data for our analytics.') } - - { _t('Learn more about how we use analytics.') } - - { ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) } -
    -
    ; - }, - - _renderLabs: function() { - const features = []; - SettingsStore.getLabsFeatures().forEach((featureId) => { - // TODO: this ought to be a separate component so that we don't need - // to rebind the onChange each time we render - const onChange = async(e) => { - const checked = e.target.checked; - if (featureId === "feature_lazyloading") { - const confirmed = await this._onLazyLoadChanging(checked); - if (!confirmed) { - e.preventDefault(); - return; - } - } - await SettingsStore.setFeatureEnabled(featureId, checked); - this.forceUpdate(); - }; - - features.push( -
    - - -
    ); - }); - - // No labs section when there are no features in labs - if (features.length === 0) { - return null; - } - - return ( -
    -

    { _t("Labs") }

    -
    -

    { _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.

    - { features } -
    -
    - ); - }, - - _onLazyLoadChanging: async function(enabling) { - // don't prevent turning LL off when not supported - if (enabling) { - const supported = await MatrixClientPeg.get().doesServerSupportLazyLoading(); - if (!supported) { - await new Promise((resolve) => { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { - title: _t("Lazy loading members not supported"), - description: -
    - { _t("Lazy loading is not supported by your " + - "current homeserver.") } -
    , - button: _t("OK"), - onFinished: resolve, - }); - }); - return false; - } - } - return true; - }, - - _renderDeactivateAccount: function() { - return
    -

    { _t("Deactivate Account") }

    -
    - { _t("Deactivate my account") } - -
    -
    ; - }, - - _renderTermsAndConditionsLinks: function() { - if (SdkConfig.get().terms_and_conditions_links) { - const tncLinks = []; - for (const tncEntry of SdkConfig.get().terms_and_conditions_links) { - tncLinks.push(); - } - return
    -

    { _t("Legal") }

    -
    - {tncLinks} -
    -
    ; - } else { - return null; - } - }, - - _renderClearCache: function() { - return
    -

    { _t("Clear Cache") }

    -
    - - { _t("Clear Cache and Reload") } - -
    -
    ; - }, - - _renderCheckUpdate: function() { - const platform = PlatformPeg.get(); - if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) { - return
    -

    { _t('Updates') }

    -
    - - { _t('Check for update') } - -
    -
    ; - } - return
    ; - }, - - _renderBulkOptions: function() { - const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => { - return r.hasMembershipState(this._me, "invite"); - }); - if (invitedRooms.length === 0) { - return null; - } - - const Spinner = sdk.getComponent("elements.Spinner"); - - let reject = ; - if (!this.state.rejectingInvites) { - // bind() the invited rooms so any new invites that may come in as this button is clicked - // don't inadvertently get rejected as well. - const onClick = this._onRejectAllInvitesClicked.bind(this, invitedRooms); - reject = ( - - { _t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length}) } - - ); - } - - return
    -

    { _t("Bulk Options") }

    -
    - { reject } -
    -
    ; - }, - - _renderElectronSettings: function() { - const settings = this.state.electron_settings; - if (!settings) return; - - // TODO: This should probably be a granular setting, but it only applies to electron - // and ends up being get/set outside of matrix anyways (local system setting). - return
    -

    { _t('Desktop specific') }

    -
    -
    - - -
    -
    -
    ; - }, - - _onAutoLaunchChanged: function(e) { - const {ipcRenderer} = require('electron'); - ipcRenderer.send('settings_set', 'auto-launch', e.target.checked); - }, - - _mapWebRtcDevicesToSpans: function(devices) { - return devices.map((device) => { device.label }); - }, - - _setAudioOutput: function(deviceId) { - this.setState({activeAudioOutput: deviceId}); - CallMediaHandler.setAudioOutput(deviceId); - }, - - _setAudioInput: function(deviceId) { - this.setState({activeAudioInput: deviceId}); - CallMediaHandler.setAudioInput(deviceId); - }, - - _setVideoInput: function(deviceId) { - this.setState({activeVideoInput: deviceId}); - CallMediaHandler.setVideoInput(deviceId); - }, - - _requestMediaPermissions: function(event) { - 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'), - }); - }, - ]); - } - }, - - _renderWebRtcDeviceSettings: function() { - if (this.state.mediaDevices === false) { - return ( - - { _t('Missing Media Permissions, click here to request.') } - - ); - } else if (!this.state.mediaDevices) return; - - const Dropdown = sdk.getComponent('elements.Dropdown'); - - let speakerDropdown =

    { _t('No Audio Outputs detected') }

    ; - let microphoneDropdown =

    { _t('No Microphones detected') }

    ; - let webcamDropdown =

    { _t('No Webcams detected') }

    ; - - const defaultOption = { - deviceId: '', - label: _t('Default Device'), - }; - - const audioOutputs = this.state.mediaDevices.audiooutput.slice(0); - if (audioOutputs.length > 0) { - let defaultOutput = ''; - if (!audioOutputs.some((input) => input.deviceId === 'default')) { - audioOutputs.unshift(defaultOption); - } else { - defaultOutput = 'default'; - } - - speakerDropdown =
    -

    { _t('Audio Output') }

    - - { this._mapWebRtcDevicesToSpans(audioOutputs) } - -
    ; - } - - const audioInputs = this.state.mediaDevices.audioinput.slice(0); - if (audioInputs.length > 0) { - let defaultInput = ''; - if (!audioInputs.some((input) => input.deviceId === 'default')) { - audioInputs.unshift(defaultOption); - } else { - defaultInput = 'default'; - } - - microphoneDropdown =
    -

    { _t('Microphone') }

    - - { this._mapWebRtcDevicesToSpans(audioInputs) } - -
    ; - } - - const videoInputs = this.state.mediaDevices.videoinput.slice(0); - if (videoInputs.length > 0) { - let defaultInput = ''; - if (!videoInputs.some((input) => input.deviceId === 'default')) { - videoInputs.unshift(defaultOption); - } else { - defaultInput = 'default'; - } - - webcamDropdown =
    -

    { _t('Camera') }

    - - { this._mapWebRtcDevicesToSpans(videoInputs) } - -
    ; - } - - return
    - { speakerDropdown } - { microphoneDropdown } - { webcamDropdown } -
    ; - }, - - _renderWebRtcSettings: function() { - return
    -

    { _t('VoIP') }

    -
    - { WEBRTC_SETTINGS.map(this._renderDeviceSetting) } - { PlatformPeg.get().isElectron() && this._renderPushToTalkSettings() } - { this._renderWebRtcDeviceSettings() } -
    -
    ; - }, - - onSelfShareClick: function() { - const cli = MatrixClientPeg.get(); - const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); - Modal.createTrackedDialog('share self dialog', '', ShareDialog, { - target: cli.getUser(this._me), - }); - }, - - _showSpoiler: function(event) { - const target = event.target; - target.innerHTML = target.getAttribute('data-spoiler'); - - const range = document.createRange(); - range.selectNodeContents(target); - - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - }, - - nameForMedium: function(medium) { - if (medium === 'msisdn') return _t('Phone'); - if (medium === 'email') return _t('Email'); - return medium[0].toUpperCase() + medium.slice(1); - }, - - presentableTextForThreepid: function(threepid) { - if (threepid.medium === 'msisdn') { - return '+' + threepid.address; - } else { - return threepid.address; - } - }, - - render: function() { - const Loader = sdk.getComponent("elements.Spinner"); - switch (this.state.phase) { - case "UserSettings.LOADING": - return ( - - ); - case "UserSettings.DISPLAY": - break; // quit the switch to return the common state - default: - throw new Error("Unknown state.phase => " + this.state.phase); - } - // can only get here if phase is UserSettings.DISPLAY - const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); - const ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName"); - const ChangePassword = sdk.getComponent("views.settings.ChangePassword"); - const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); - const Notifications = sdk.getComponent("settings.Notifications"); - const EditableText = sdk.getComponent('elements.EditableText'); - const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); - - const avatarUrl = ( - this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null - ); - - const threepidsSection = this.state.threepids.map((val, pidIndex) => { - const id = "3pid-" + val.address; - // TODO: make a separate component to avoid having to rebind onClick - // each time we render - const onRemoveClick = (e) => this.onRemoveThreepidClicked(val); - return ( -
    -
    - -
    -
    - -
    -
    - -
    -
    - ); - }); - let addEmailSection; - if (this.state.email_add_pending) { - addEmailSection = ; - } else { - addEmailSection = ( -
    -
    - -
    -
    - -
    -
    - -
    -
    - ); - } - const AddPhoneNumber = sdk.getComponent('views.settings.AddPhoneNumber'); - const addMsisdnSection = ( - - ); - threepidsSection.push(addEmailSection); - threepidsSection.push(addMsisdnSection); - - const accountJsx = ( - - ); - - let notificationArea; - if (this.state.threepids !== undefined) { - notificationArea = (
    -

    { _t("Notifications") }

    - -
    - -
    -
    ); - } - - const olmVersion = MatrixClientPeg.get().olmVersion; - // If the olmVersion is not defined then either crypto is disabled, or - // we are using a version old version of olm. We assume the former. - let olmVersionString = ""; - if (olmVersion) { - olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`; - } - - return ( -
    - - - - -

    { _t("Profile") }

    - -
    -
    -
    -
    - -
    -
    - -
    -
    - { threepidsSection } -
    - -
    - - {_t("Remove - -
    - -
    -
    - - -
    -
    -
    - -

    { _t("Account") }

    - -
    - - { _t("Sign out") } - - { this.state.userHasGeneratedPassword ? -
    - { _t("To return to your account in future you need to set a password") } -
    : null - } - - { accountJsx } -
    - - { this._renderGroupSettings() } - - { this._renderReferral() } - - { notificationArea } - - { this._renderUserInterfaceSettings() } - { this._renderLabs() } - { this._renderWebRtcSettings() } - { this._renderDevicesPanel() } - { this._renderCryptoInfo() } - { this._renderIgnoredUsers() } - { this._renderBulkOptions() } - { this._renderBugReport() } - - { PlatformPeg.get().isElectron() && this._renderElectronSettings() } - - { this._renderAnalyticsControl() } - -

    { _t("Advanced") }

    - -
    -
    - { _t("Logged in as:") + ' ' } - - { this._me } - -
    -
    - { _t('Access Token:') + ' ' } - - <{ _t("click to reveal") }> - -
    -
    - { _t("Homeserver is") } { MatrixClientPeg.get().getHomeserverUrl() } -
    -
    - { _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() } -
    -
    - { _t('matrix-react-sdk version:') } { (REACT_SDK_VERSION !== '') - ? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) - : REACT_SDK_VERSION - }
    - { _t('riot-web version:') } { (this.state.vectorVersion !== undefined) - ? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion) - : 'unknown' - }
    - { _t("olm version:") } { olmVersionString }
    -
    -
    - - { this._renderCheckUpdate() } - - { this._renderClearCache() } - - { this._renderDeactivateAccount() } - - { this._renderTermsAndConditionsLinks() } - -
    -
    - ); - }, -}); diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index aefb114dd32..7a04bf8aca7 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -19,10 +19,12 @@ import {_t} from "../../../../languageHandler"; import CallMediaHandler from "../../../../CallMediaHandler"; import Field from "../../elements/Field"; import AccessibleButton from "../../elements/AccessibleButton"; -import {SettingLevel} from "../../../../settings/SettingsStore"; +import {SettingLevel, getValue, getValueAt, setValue, getDisplayName} from "../../../../settings/SettingsStore"; +import PushToTalk from "../../../../PushToTalk"; const Modal = require("../../../../Modal"); const sdk = require("../../../../index"); const MatrixClientPeg = require("../../../../MatrixClientPeg"); +const PlatformPeg = require("../../../../PlatformPeg"); export default class VoiceSettingsTab extends React.Component { constructor() { @@ -33,6 +35,11 @@ export default class VoiceSettingsTab extends React.Component { activeAudioOutput: null, activeAudioInput: null, activeVideoInput: null, + listenKeydown: null, + listenKeyup: null, + pushToTalkAscii: getValue('pushToTalk').ascii, + pushToTalkKeybinding: getValue('pushToTalk').keybinding, + pushToTalkEnabled: getValue('pushToTalk').enabled, }; } @@ -40,6 +47,199 @@ export default class VoiceSettingsTab extends React.Component { this._refreshMediaDevices(); } + componentWillUnmount(): void { + if (PlatformPeg.get().isElectron()) { + const {ipcRenderer} = require('electron'); + ipcRenderer.removeListener('settings', this._electronSettings); + + // Stop recording push-to-talk shortcut if tab is closed + this._stopRecordingGlobalShortcut(); + } + } + + _translateKeybinding(key) { + // Custom translations to make keycodes look nicer + + // KeyA -> A + if (key.startsWith('Key')) { + key = key.substring(3); + } + + return key; + } + + _startRecordingGlobalShortcut(asciiStateKey, codeStateKey) { + const keyAscii = []; + const keyCodes = []; + const self = this; + + // Record keypresses using KeyboardEvent + // Used for displaying ascii-representation of current keys + // in the UI + this.setState({listenKeydown: function(event) { + // TODO: Show RightShift and things + const key = self._translateKeybinding(event.code); + const index = keyAscii.indexOf(key); + if (index === -1) { + keyAscii.push(key); + self.setState({pushToTalkAscii: keyAscii.join(' + ')}); + } + event.preventDefault(); + }}); + this.setState({listenKeyup: function(event) { + const index = keyAscii.indexOf(self._translateKeybinding(event.code)); + if (index !== -1) { + keyAscii.splice(index, 1); + } + event.preventDefault(); + }}); + + window.addEventListener("keydown", this.state.listenKeydown); + window.addEventListener("keyup", this.state.listenKeyup); + + // Record keypresses using iohook + // Used for getting keycode-representation of current keys + // for later global shortcut registration + const {ipcRenderer} = require('electron'); + ipcRenderer.send('start-listening-keys'); + + // When a key is pressed, add all current pressed keys to the shortcut + // When a key is lifted, don't remove it from the shortcut + + // This enables a nicer shortcut-recording experience, as the user can + // press down their desired keys, release them, and then save the + // shortcut without all the keys disappearing + ipcRenderer.on('keypress', function(ev, event) { + if (event.keydown) { + const index = keyCodes.indexOf(event.keycode); + if (index === -1) { + keyCodes.push(event.keycode); + // slice is needed here to save a new copy of the keycodes + // array to the state, else if we update keycodes later, it + // still updates the state since the state has a ref to this array + self.setState({pushToTalkKeybinding: keyCodes.slice()}); + } + } else { + const index = keyCodes.indexOf(event.keycode); + if (index !== -1) { + keyCodes.splice(index, 1); + } + } + }); + + // Stop recording shortcut if window loses focus + ipcRenderer.on('window-blurred', () => { + if (this.state.settingKeybinding) { + this._stopRecordingGlobalShortcut(); + } + }); + } + + _stopRecordingGlobalShortcut() { + // Stop recording KeyboardEvent keypresses + window.removeEventListener("keydown", this.state.listenKeydown); + window.removeEventListener("keyup", this.state.listenKeyup); + + // Stop recording iohook keypresses + const {ipcRenderer} = require('electron'); + ipcRenderer.send('stop-listening-keys'); + + this.setState({settingKeybinding: false}); + } + + _onSetPushToTalkClicked() { + // Either record or save a new shortcut + const id = 'pushToTalk'; + const currentPTTState = getValue(id); + + // Determine if we're reading shortcuts or setting them + if (!this.state.settingKeybinding) { + // Start listening for keypresses and show current + // held shortcut on screen + // Run some sort of function that just loops until the state changes back to + // not setting + this.state.pushToTalkAscii = 'Press Keys'; + this._startRecordingGlobalShortcut('pushToTalkAscii', 'pushToTalkKeybinding'); + } else { + this._stopRecordingGlobalShortcut(); + + // Disable and unregister old shortcut + PushToTalk.disable(); + + // Set the keybinding they've currently selected + currentPTTState.keybinding = this.state.pushToTalkKeybinding; + currentPTTState.ascii = this.state.pushToTalkAscii; + + // Update push to talk keybinding + setValue(id, null, SettingLevel.DEVICE, currentPTTState); + + // Enable and register new shortcut + PushToTalk.enable(currentPTTState.keybinding); + } + + // Toggle setting state + this.setState({settingKeybinding: !this.state.settingKeybinding}); + } + + _onTogglePushToTalkClicked(e) { + // Enable or disable push to talk functionality + const id = 'pushToTalk'; + const currentPTTState = getValueAt(SettingLevel.DEVICE, id); + + if (e.target.checked) { + // Enable push to talk + + this.setState({pushToTalkEnabled: true}); + currentPTTState.enabled = true; + setValue(id, null, SettingLevel.DEVICE, currentPTTState); + + PushToTalk.enable(currentPTTState.keybinding); + } else { + // Disable push to talk + console.log("Disabling push to talk..."); + + this.setState({pushToTalkEnabled: false}); + currentPTTState.enabled = false; + setValue(id, null, SettingLevel.DEVICE, currentPTTState); + this.setState({pushToTalkKeybinding: []}); + + PushToTalk.disable(); + } + } + + _renderPushToTalkSettings() { + const id = "pushToTalk"; + const buttonLabel = this.state.settingKeybinding ? 'Stop' : 'Set'; + const activated = getValueAt(SettingLevel.DEVICE, id).enabled; + + return ( +
    + + + + + + + + +
    + + + {"Shortcut: " + this.state.pushToTalkAscii} + +
    +
    + ); + } + _refreshMediaDevices = async (stream) => { if (stream) { // kill stream so that we don't leave it lingering around with webcam enabled etc @@ -101,6 +301,7 @@ export default class VoiceSettingsTab extends React.Component { let speakerDropdown = null; let microphoneDropdown = null; let webcamDropdown = null; + let pushToTalk = null; if (this.state.mediaDevices === false) { requestButton = (
    @@ -165,6 +366,33 @@ export default class VoiceSettingsTab extends React.Component { } } + if (PlatformPeg.get().isElectron()) { + const id = "pushToTalk"; + const buttonLabel = this.state.settingKeybinding ? _t('Stop') : _t('Set'); + const activated = getValueAt(SettingLevel.DEVICE, id).enabled; + + pushToTalk = ( +
    + + + + + {" | Shortcut: " + this.state.pushToTalkAscii + " "} + + + +
    + ); + } + return (
    {_t("Voice & Video")}
    @@ -173,6 +401,7 @@ export default class VoiceSettingsTab extends React.Component { {speakerDropdown} {microphoneDropdown} {webcamDropdown} + {pushToTalk}
    From 177d73c82ca68e135881991993d959d77b07e4d0 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 21 Feb 2019 20:56:09 +0000 Subject: [PATCH 07/25] Remove small fixes, electron addage --- package.json | 1 - src/settings/Settings.js | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 3db56306c36..b0b22369d01 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "classnames": "^2.1.2", "commonmark": "^0.28.1", "counterpart": "^0.18.0", - "electron": "^4.0.4", "emojione": "2.2.7", "file-saver": "^1.3.3", "filesize": "3.5.6", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 3841049fb9f..84d3606d6c8 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -89,7 +89,7 @@ export const SETTINGS = { // }, "feature_pinning": { isFeature: true, - displayName: _td('Message Pinning'), + displayName: _td("Message Pinning"), supportedLevels: LEVELS_FEATURE, default: false, }, @@ -295,21 +295,21 @@ export const SETTINGS = { supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM, displayName: { "default": _td('Enable inline URL previews by default'), - "room-account": _td('Enable URL previews for this room (only affects you)'), - "room": _td('Enable URL previews by default for participants in this room'), + "room-account": _td("Enable URL previews for this room (only affects you)"), + "room": _td("Enable URL previews by default for participants in this room"), }, default: true, }, "urlPreviewsEnabled_e2ee": { supportedLevels: ['room-device', 'room-account'], displayName: { - "room-account": _td('Enable URL previews for this room (only affects you)'), + "room-account": _td("Enable URL previews for this room (only affects you)"), }, default: false, }, "roomColor": { supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM, - displayName: _td('Room Colour'), + displayName: _td("Room Colour"), default: { primary_color: null, // Hex string, eg: #000000 secondary_color: null, // Hex string, eg: #000000 From 964e85033a61569450a4dc3ed19271037ea3b1be Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 21 Feb 2019 23:24:14 +0000 Subject: [PATCH 08/25] hey it all works --- .../views/settings/tabs/VoiceSettingsTab.js | 89 ++++++++++--------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index 7a04bf8aca7..55671e02f05 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -19,13 +19,16 @@ import {_t} from "../../../../languageHandler"; import CallMediaHandler from "../../../../CallMediaHandler"; import Field from "../../elements/Field"; import AccessibleButton from "../../elements/AccessibleButton"; -import {SettingLevel, getValue, getValueAt, setValue, getDisplayName} from "../../../../settings/SettingsStore"; -import PushToTalk from "../../../../PushToTalk"; +import ToggleSwitch from "../../elements/ToggleSwitch"; +import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; const Modal = require("../../../../Modal"); const sdk = require("../../../../index"); const MatrixClientPeg = require("../../../../MatrixClientPeg"); const PlatformPeg = require("../../../../PlatformPeg"); +let listenKeydown = null; +let listenKeyup = null; + export default class VoiceSettingsTab extends React.Component { constructor() { super(); @@ -35,11 +38,9 @@ export default class VoiceSettingsTab extends React.Component { activeAudioOutput: null, activeAudioInput: null, activeVideoInput: null, - listenKeydown: null, - listenKeyup: null, - pushToTalkAscii: getValue('pushToTalk').ascii, - pushToTalkKeybinding: getValue('pushToTalk').keybinding, - pushToTalkEnabled: getValue('pushToTalk').enabled, + pushToTalkAscii: SettingsStore.getValue('pushToTalk').ascii, + pushToTalkKeybinding: SettingsStore.getValue('pushToTalk').keybinding, + pushToTalkEnabled: SettingsStore.getValue('pushToTalk').enabled, }; } @@ -48,16 +49,15 @@ export default class VoiceSettingsTab extends React.Component { } componentWillUnmount(): void { - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - ipcRenderer.removeListener('settings', this._electronSettings); + if (PlatformPeg.get().supportsAutoLaunch()) { + //ipcRenderer.get().removeListener('settings', this._electronSettings); - // Stop recording push-to-talk shortcut if tab is closed + // Stop recording push-to-talk shortcut if Settings or tab is closed this._stopRecordingGlobalShortcut(); } } - _translateKeybinding(key) { + _translateKeybinding = (key) => { // Custom translations to make keycodes look nicer // KeyA -> A @@ -68,7 +68,7 @@ export default class VoiceSettingsTab extends React.Component { return key; } - _startRecordingGlobalShortcut(asciiStateKey, codeStateKey) { + _startRecordingGlobalShortcut = (asciiStateKey, codeStateKey) => { const keyAscii = []; const keyCodes = []; const self = this; @@ -76,7 +76,7 @@ export default class VoiceSettingsTab extends React.Component { // Record keypresses using KeyboardEvent // Used for displaying ascii-representation of current keys // in the UI - this.setState({listenKeydown: function(event) { + listenKeydown = (event) => { // TODO: Show RightShift and things const key = self._translateKeybinding(event.code); const index = keyAscii.indexOf(key); @@ -85,23 +85,23 @@ export default class VoiceSettingsTab extends React.Component { self.setState({pushToTalkAscii: keyAscii.join(' + ')}); } event.preventDefault(); - }}); - this.setState({listenKeyup: function(event) { + }; + + listenKeyup = (event) => { const index = keyAscii.indexOf(self._translateKeybinding(event.code)); if (index !== -1) { keyAscii.splice(index, 1); } event.preventDefault(); - }}); + }; - window.addEventListener("keydown", this.state.listenKeydown); - window.addEventListener("keyup", this.state.listenKeyup); + window.addEventListener("keydown", listenKeydown); + window.addEventListener("keyup", listenKeyup); // Record keypresses using iohook // Used for getting keycode-representation of current keys // for later global shortcut registration - const {ipcRenderer} = require('electron'); - ipcRenderer.send('start-listening-keys'); + PlatformPeg.get().startListeningKeys(); // When a key is pressed, add all current pressed keys to the shortcut // When a key is lifted, don't remove it from the shortcut @@ -109,7 +109,7 @@ export default class VoiceSettingsTab extends React.Component { // This enables a nicer shortcut-recording experience, as the user can // press down their desired keys, release them, and then save the // shortcut without all the keys disappearing - ipcRenderer.on('keypress', function(ev, event) { + PlatformPeg.get().onKeypress(this, (ev, event) => { if (event.keydown) { const index = keyCodes.indexOf(event.keycode); if (index === -1) { @@ -117,7 +117,7 @@ export default class VoiceSettingsTab extends React.Component { // slice is needed here to save a new copy of the keycodes // array to the state, else if we update keycodes later, it // still updates the state since the state has a ref to this array - self.setState({pushToTalkKeybinding: keyCodes.slice()}); + this.setState({pushToTalkKeybinding: keyCodes.slice()}); } } else { const index = keyCodes.indexOf(event.keycode); @@ -128,29 +128,30 @@ export default class VoiceSettingsTab extends React.Component { }); // Stop recording shortcut if window loses focus - ipcRenderer.on('window-blurred', () => { + PlatformPeg.get().onWindowBlurred(() => { if (this.state.settingKeybinding) { - this._stopRecordingGlobalShortcut(); + // TODO: Figure out why listener is not a function + //this._stopRecordingGlobalShortcut(); } }); } - _stopRecordingGlobalShortcut() { + _stopRecordingGlobalShortcut = () => { // Stop recording KeyboardEvent keypresses - window.removeEventListener("keydown", this.state.listenKeydown); - window.removeEventListener("keyup", this.state.listenKeyup); + window.removeEventListener("keydown", listenKeydown); + window.removeEventListener("keyup", listenKeyup); // Stop recording iohook keypresses - const {ipcRenderer} = require('electron'); - ipcRenderer.send('stop-listening-keys'); + PlatformPeg.get().stopListeningKeys(); this.setState({settingKeybinding: false}); } - _onSetPushToTalkClicked() { + _onSetPushToTalkClicked = () => { // Either record or save a new shortcut + const PushToTalk = require('../../../../PushToTalk'); const id = 'pushToTalk'; - const currentPTTState = getValue(id); + const currentPTTState = SettingsStore.getValue(id); // Determine if we're reading shortcuts or setting them if (!this.state.settingKeybinding) { @@ -171,7 +172,7 @@ export default class VoiceSettingsTab extends React.Component { currentPTTState.ascii = this.state.pushToTalkAscii; // Update push to talk keybinding - setValue(id, null, SettingLevel.DEVICE, currentPTTState); + SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); // Enable and register new shortcut PushToTalk.enable(currentPTTState.keybinding); @@ -181,36 +182,36 @@ export default class VoiceSettingsTab extends React.Component { this.setState({settingKeybinding: !this.state.settingKeybinding}); } - _onTogglePushToTalkClicked(e) { + _onTogglePushToTalkClicked = (e) => { // Enable or disable push to talk functionality + const PushToTalk = require('../../../../PushToTalk'); const id = 'pushToTalk'; - const currentPTTState = getValueAt(SettingLevel.DEVICE, id); + const currentPTTState = SettingsStore.getValueAt(SettingLevel.DEVICE, id); if (e.target.checked) { // Enable push to talk this.setState({pushToTalkEnabled: true}); currentPTTState.enabled = true; - setValue(id, null, SettingLevel.DEVICE, currentPTTState); + SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); PushToTalk.enable(currentPTTState.keybinding); } else { // Disable push to talk - console.log("Disabling push to talk..."); this.setState({pushToTalkEnabled: false}); currentPTTState.enabled = false; - setValue(id, null, SettingLevel.DEVICE, currentPTTState); + SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); this.setState({pushToTalkKeybinding: []}); PushToTalk.disable(); } } - _renderPushToTalkSettings() { + _renderPushToTalkSettings = () => { const id = "pushToTalk"; const buttonLabel = this.state.settingKeybinding ? 'Stop' : 'Set'; - const activated = getValueAt(SettingLevel.DEVICE, id).enabled; + const activated = SettingsStore.getValueAt(SettingLevel.DEVICE, id).enabled; return (
    @@ -223,7 +224,7 @@ export default class VoiceSettingsTab extends React.Component { defaultChecked={activated} onChange={this._onTogglePushToTalkClicked} /> - + {"Shortcut: " + this.state.pushToTalkAscii} @@ -366,10 +367,10 @@ export default class VoiceSettingsTab extends React.Component { } } - if (PlatformPeg.get().isElectron()) { + if (PlatformPeg.get().supportsAutoLaunch()) { const id = "pushToTalk"; const buttonLabel = this.state.settingKeybinding ? _t('Stop') : _t('Set'); - const activated = getValueAt(SettingLevel.DEVICE, id).enabled; + const activated = SettingsStore.getValueAt(SettingLevel.DEVICE, id).enabled; pushToTalk = (
    @@ -379,7 +380,7 @@ export default class VoiceSettingsTab extends React.Component { defaultChecked={activated} onChange={this._onTogglePushToTalkClicked} /> - + {" | Shortcut: " + this.state.pushToTalkAscii + " "} From 67a0cd54ec8597e391430748be21dfe5f76a18c7 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 22 Feb 2019 16:03:46 +0000 Subject: [PATCH 09/25] Getting there --- src/PushToTalk.js | 37 ++++- .../views/elements/PersistedElement.js | 8 +- .../views/settings/tabs/VoiceSettingsTab.js | 138 ++++++++---------- src/i18n/strings/en_EN.json | 3 + 4 files changed, 99 insertions(+), 87 deletions(-) diff --git a/src/PushToTalk.js b/src/PushToTalk.js index 378fc9fe937..a8394331592 100644 --- a/src/PushToTalk.js +++ b/src/PushToTalk.js @@ -18,10 +18,14 @@ import PlatformPeg from './PlatformPeg'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import SettingsStore from './settings/SettingsStore'; -export function enable(keybinding) { - const id = 'pushToTalk'; +export const id = "pushToTalk"; + +export function startListening() { + console.log("START LISTENING!") + const keybinding = SettingsStore.getValue(id).keybinding; PlatformPeg.get().addGlobalKeybinding(id, keybinding, () => { const widgetId = ActiveWidgetStore.getPersistentWidgetId(); + console.log("Key pressed in JS") // Only try to un/mute if jitsi is onscreen if (widgetId === null || widgetId === undefined) { @@ -32,6 +36,7 @@ export function enable(keybinding) { widgetMessaging.unmuteJitsiAudio(); }, () => { const widgetId = ActiveWidgetStore.getPersistentWidgetId(); + console.log("Key released in JS") // Only try to un/mute if jitsi is onscreen if (widgetId === null || widgetId === undefined) { @@ -41,10 +46,34 @@ export function enable(keybinding) { const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); widgetMessaging.muteJitsiAudio(); }); + + PlatformPeg.get().startListeningKeys(); } -export function disable() { - const id = 'pushToTalk'; +export function stopListening() { + console.log("STOP LISTENING!") + PlatformPeg.get().stopListeningKeys(); + const keybinding = SettingsStore.getValue(id).keybinding; PlatformPeg.get().removeGlobalKeybinding(id, keybinding); } + +export function setKeybinding(keybinding) { + const currentPTTState = SettingsStore.getValue(id); + currentPTTState.keybinding = keybinding; + SettingsStore.setValue(id, currentPTTState); +} + +function setEnabled(enabled: boolean) { + const currentPTTState = SettingsStore.getValue(id); + currentPTTState.enabled = enabled; + SettingsStore.setValue(id, currentPTTState); +} + +export function enable() { + setEnabled(true); +} + +export function disable() { + setEnabled(false); +} diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index 1f1b17e98eb..b38ba0415ff 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -117,8 +117,8 @@ export default class PersistedElement extends React.Component { // Start Push-To-Talk service when Jitsi widget is mounted // TODO: This seems quite hacky - is there a better way to // check if this is a Jitsi vs. StickerPicker widget? - if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue('pushToTalk').enabled) { - PushToTalk.enable(SettingsStore.getValue('pushToTalk').keybinding); + if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { + PushToTalk.startListening(); } this.updateChild(); @@ -135,8 +135,8 @@ export default class PersistedElement extends React.Component { dis.unregister(this._dispatcherRef); // Stop Push-To-Talk service when Jitsi widget is unmounted - if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue('pushToTalk').enabled) { - PushToTalk.disable(); + if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { + PushToTalk.stopListening(); } } diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index 55671e02f05..dfb02b51597 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -21,6 +21,7 @@ import Field from "../../elements/Field"; import AccessibleButton from "../../elements/AccessibleButton"; import ToggleSwitch from "../../elements/ToggleSwitch"; import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; +const PushToTalk = require('../../../../PushToTalk'); const Modal = require("../../../../Modal"); const sdk = require("../../../../index"); const MatrixClientPeg = require("../../../../MatrixClientPeg"); @@ -38,9 +39,9 @@ export default class VoiceSettingsTab extends React.Component { activeAudioOutput: null, activeAudioInput: null, activeVideoInput: null, - pushToTalkAscii: SettingsStore.getValue('pushToTalk').ascii, - pushToTalkKeybinding: SettingsStore.getValue('pushToTalk').keybinding, - pushToTalkEnabled: SettingsStore.getValue('pushToTalk').enabled, + pushToTalkAscii: SettingsStore.getValue(PushToTalk.id).ascii, + pushToTalkKeybinding: SettingsStore.getValue(PushToTalk.id).keybinding, + pushToTalkEnabled: SettingsStore.getValue(PushToTalk.id).enabled, }; } @@ -65,6 +66,14 @@ export default class VoiceSettingsTab extends React.Component { key = key.substring(3); } + // Direct translations + // TODO: Just use a map + if (key == "ShiftLeft") { + key = "Shift"; + } else if (key == "AltLeft") { + key = "Alt"; + } + return key; } @@ -77,7 +86,6 @@ export default class VoiceSettingsTab extends React.Component { // Used for displaying ascii-representation of current keys // in the UI listenKeydown = (event) => { - // TODO: Show RightShift and things const key = self._translateKeybinding(event.code); const index = keyAscii.indexOf(key); if (index === -1) { @@ -109,7 +117,8 @@ export default class VoiceSettingsTab extends React.Component { // This enables a nicer shortcut-recording experience, as the user can // press down their desired keys, release them, and then save the // shortcut without all the keys disappearing - PlatformPeg.get().onKeypress(this, (ev, event) => { + PlatformPeg.get().onKeypress((ev, event) => { + console.log("GOT KEYPRESS!") if (event.keydown) { const index = keyCodes.indexOf(event.keycode); if (index === -1) { @@ -129,8 +138,9 @@ export default class VoiceSettingsTab extends React.Component { // Stop recording shortcut if window loses focus PlatformPeg.get().onWindowBlurred(() => { + console.log("Setting keybinding:", this.state.settingKeybinding) if (this.state.settingKeybinding) { - // TODO: Figure out why listener is not a function + // TODO: Stop this from killing PTT on blur //this._stopRecordingGlobalShortcut(); } }); @@ -142,66 +152,57 @@ export default class VoiceSettingsTab extends React.Component { window.removeEventListener("keyup", listenKeyup); // Stop recording iohook keypresses - PlatformPeg.get().stopListeningKeys(); + PushToTalk.stopListening(); this.setState({settingKeybinding: false}); } _onSetPushToTalkClicked = () => { // Either record or save a new shortcut - const PushToTalk = require('../../../../PushToTalk'); - const id = 'pushToTalk'; - const currentPTTState = SettingsStore.getValue(id); + const currentPTTState = SettingsStore.getValue(PushToTalk.id); // Determine if we're reading shortcuts or setting them if (!this.state.settingKeybinding) { - // Start listening for keypresses and show current - // held shortcut on screen - // Run some sort of function that just loops until the state changes back to - // not setting + // Start listening for keypresses and show current held shortcut on + // screen + this.state.pushToTalkAscii = 'Press Keys'; this._startRecordingGlobalShortcut('pushToTalkAscii', 'pushToTalkKeybinding'); } else { this._stopRecordingGlobalShortcut(); - // Disable and unregister old shortcut - PushToTalk.disable(); + // TODO: Combine the below into setKeybinding // Set the keybinding they've currently selected currentPTTState.keybinding = this.state.pushToTalkKeybinding; currentPTTState.ascii = this.state.pushToTalkAscii; // Update push to talk keybinding - SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); + SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); - // Enable and register new shortcut - PushToTalk.enable(currentPTTState.keybinding); + // Register new shortcut + PushToTalk.setKeybinding(currentPTTState.keybinding); } // Toggle setting state this.setState({settingKeybinding: !this.state.settingKeybinding}); } - _onTogglePushToTalkClicked = (e) => { + _onTogglePushToTalkClicked = (enabled) => { // Enable or disable push to talk functionality - const PushToTalk = require('../../../../PushToTalk'); - const id = 'pushToTalk'; - const currentPTTState = SettingsStore.getValueAt(SettingLevel.DEVICE, id); - if (e.target.checked) { + if (enabled) { // Enable push to talk + PlatformPeg.get().setupPTT(); this.setState({pushToTalkEnabled: true}); - currentPTTState.enabled = true; - SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); - PushToTalk.enable(currentPTTState.keybinding); + PushToTalk.enable(); + PushToTalk.setKeybinding(SettingsStore.getValue(PushToTalk.id).keybinding); } else { // Disable push to talk this.setState({pushToTalkEnabled: false}); - currentPTTState.enabled = false; - SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); this.setState({pushToTalkKeybinding: []}); PushToTalk.disable(); @@ -209,34 +210,36 @@ export default class VoiceSettingsTab extends React.Component { } _renderPushToTalkSettings = () => { - const id = "pushToTalk"; - const buttonLabel = this.state.settingKeybinding ? 'Stop' : 'Set'; - const activated = SettingsStore.getValueAt(SettingLevel.DEVICE, id).enabled; - - return ( -
    - - - - - - + +
    - - - {"Shortcut: " + this.state.pushToTalkAscii} -
    +
    + ); + } + + return ( +
    +
    + {SettingsStore.getDisplayName(PushToTalk.id)} + +
    + {setShortcut}
    ); } @@ -258,7 +261,7 @@ export default class VoiceSettingsTab extends React.Component { _requestMediaPermissions = () => { const getUserMedia = ( - window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia + window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mediaDevices.mozGetUserMedia ); if (getUserMedia) { return getUserMedia.apply(window.navigator, [ @@ -367,31 +370,8 @@ export default class VoiceSettingsTab extends React.Component { } } - if (PlatformPeg.get().supportsAutoLaunch()) { - const id = "pushToTalk"; - const buttonLabel = this.state.settingKeybinding ? _t('Stop') : _t('Set'); - const activated = SettingsStore.getValueAt(SettingLevel.DEVICE, id).enabled; - - pushToTalk = ( -
    - - - - - {" | Shortcut: " + this.state.pushToTalkAscii + " "} - - - -
    - ); + if (PlatformPeg.get().supportsAutoLaunch()) { // If electron + pushToTalk = this._renderPushToTalkSettings(); } return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 84c9dacd07f..25856c779bf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -273,6 +273,7 @@ "Failed to join room": "Failed to join room", "Message Pinning": "Message Pinning", "Custom user status messages": "Custom user status messages", + "Push-to-Talk": "Push-to-Talk", "Show recent room avatars above the room list (refresh to apply changes)": "Show recent room avatars above the room list (refresh to apply changes)", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", @@ -612,6 +613,8 @@ "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", "Learn more about how we use analytics.": "Learn more about how we use analytics.", + "Stop": "Stop", + "Set": "Set", "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.", From 22663c12e51124b665adb5453e6b8a712dc675b3 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 22 Feb 2019 18:19:17 +0000 Subject: [PATCH 10/25] Fix issues --- src/PushToTalk.js | 33 +------------- .../views/elements/PersistedElement.js | 4 +- .../views/settings/tabs/VoiceSettingsTab.js | 45 +++++++++---------- 3 files changed, 26 insertions(+), 56 deletions(-) diff --git a/src/PushToTalk.js b/src/PushToTalk.js index a8394331592..de7f26b86ab 100644 --- a/src/PushToTalk.js +++ b/src/PushToTalk.js @@ -20,12 +20,9 @@ import SettingsStore from './settings/SettingsStore'; export const id = "pushToTalk"; -export function startListening() { - console.log("START LISTENING!") - const keybinding = SettingsStore.getValue(id).keybinding; +export function enable(keybinding) { PlatformPeg.get().addGlobalKeybinding(id, keybinding, () => { const widgetId = ActiveWidgetStore.getPersistentWidgetId(); - console.log("Key pressed in JS") // Only try to un/mute if jitsi is onscreen if (widgetId === null || widgetId === undefined) { @@ -36,7 +33,6 @@ export function startListening() { widgetMessaging.unmuteJitsiAudio(); }, () => { const widgetId = ActiveWidgetStore.getPersistentWidgetId(); - console.log("Key released in JS") // Only try to un/mute if jitsi is onscreen if (widgetId === null || widgetId === undefined) { @@ -46,34 +42,9 @@ export function startListening() { const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); widgetMessaging.muteJitsiAudio(); }); - - PlatformPeg.get().startListeningKeys(); } -export function stopListening() { - console.log("STOP LISTENING!") - PlatformPeg.get().stopListeningKeys(); - +export function disable() { const keybinding = SettingsStore.getValue(id).keybinding; PlatformPeg.get().removeGlobalKeybinding(id, keybinding); } - -export function setKeybinding(keybinding) { - const currentPTTState = SettingsStore.getValue(id); - currentPTTState.keybinding = keybinding; - SettingsStore.setValue(id, currentPTTState); -} - -function setEnabled(enabled: boolean) { - const currentPTTState = SettingsStore.getValue(id); - currentPTTState.enabled = enabled; - SettingsStore.setValue(id, currentPTTState); -} - -export function enable() { - setEnabled(true); -} - -export function disable() { - setEnabled(false); -} diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index b38ba0415ff..e221f960551 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -118,7 +118,7 @@ export default class PersistedElement extends React.Component { // TODO: This seems quite hacky - is there a better way to // check if this is a Jitsi vs. StickerPicker widget? if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { - PushToTalk.startListening(); + PushToTalk.enable(SettingsStore.getValue(PushToTalk.id).keybinding); } this.updateChild(); @@ -136,7 +136,7 @@ export default class PersistedElement extends React.Component { // Stop Push-To-Talk service when Jitsi widget is unmounted if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { - PushToTalk.stopListening(); + PushToTalk.disable(); } } diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index dfb02b51597..15f60887688 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -59,19 +59,18 @@ export default class VoiceSettingsTab extends React.Component { } _translateKeybinding = (key) => { - // Custom translations to make keycodes look nicer - // KeyA -> A if (key.startsWith('Key')) { key = key.substring(3); } - // Direct translations - // TODO: Just use a map - if (key == "ShiftLeft") { - key = "Shift"; - } else if (key == "AltLeft") { - key = "Alt"; + // Custom translations to make keycodes look nicer + const translations = { + "ShiftLeft": "Shift", + "AltLeft": "Shift", + }; + if (key in translations) { + key = translations[key]; } return key; @@ -117,8 +116,7 @@ export default class VoiceSettingsTab extends React.Component { // This enables a nicer shortcut-recording experience, as the user can // press down their desired keys, release them, and then save the // shortcut without all the keys disappearing - PlatformPeg.get().onKeypress((ev, event) => { - console.log("GOT KEYPRESS!") + PlatformPeg.get().onKeypress(this, (ev, event) => { if (event.keydown) { const index = keyCodes.indexOf(event.keycode); if (index === -1) { @@ -138,7 +136,6 @@ export default class VoiceSettingsTab extends React.Component { // Stop recording shortcut if window loses focus PlatformPeg.get().onWindowBlurred(() => { - console.log("Setting keybinding:", this.state.settingKeybinding) if (this.state.settingKeybinding) { // TODO: Stop this from killing PTT on blur //this._stopRecordingGlobalShortcut(); @@ -152,7 +149,7 @@ export default class VoiceSettingsTab extends React.Component { window.removeEventListener("keyup", listenKeyup); // Stop recording iohook keypresses - PushToTalk.stopListening(); + PlatformPeg.get().stopListeningKeys(); this.setState({settingKeybinding: false}); } @@ -171,7 +168,8 @@ export default class VoiceSettingsTab extends React.Component { } else { this._stopRecordingGlobalShortcut(); - // TODO: Combine the below into setKeybinding + // Disable and unregister old shortcut + PushToTalk.disable(); // Set the keybinding they've currently selected currentPTTState.keybinding = this.state.pushToTalkKeybinding; @@ -180,8 +178,8 @@ export default class VoiceSettingsTab extends React.Component { // Update push to talk keybinding SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); - // Register new shortcut - PushToTalk.setKeybinding(currentPTTState.keybinding); + // Enable and register new shortcut + PushToTalk.enable(currentPTTState.keybinding); } // Toggle setting state @@ -190,19 +188,24 @@ export default class VoiceSettingsTab extends React.Component { _onTogglePushToTalkClicked = (enabled) => { // Enable or disable push to talk functionality + const currentPTTState = SettingsStore.getValue(PushToTalk.id); if (enabled) { // Enable push to talk PlatformPeg.get().setupPTT(); this.setState({pushToTalkEnabled: true}); + SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); + currentPTTState.enabled = true; - PushToTalk.enable(); + PushToTalk.enable(currentPTTState.keybinding); PushToTalk.setKeybinding(SettingsStore.getValue(PushToTalk.id).keybinding); } else { // Disable push to talk this.setState({pushToTalkEnabled: false}); + currentPTTState.enabled = false; + SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); this.setState({pushToTalkKeybinding: []}); PushToTalk.disable(); @@ -217,18 +220,14 @@ export default class VoiceSettingsTab extends React.Component { if (this.state.pushToTalkEnabled) { setShortcut = (
    - - - - - -
    {"Shortcut: " + this.state.pushToTalkAscii + " "} + {"Shortcut: " + this.state.pushToTalkAscii + " "} + -
    +
    ); } From 2a41ebc0fe863c17fe0b9b5f977854704383b32b Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Sat, 23 Feb 2019 15:23:26 +0000 Subject: [PATCH 11/25] Add PTT toggle mode --- src/PushToTalk.js | 13 +++++++++++- .../views/settings/tabs/VoiceSettingsTab.js | 20 +++++++++++++++---- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 1 + 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/PushToTalk.js b/src/PushToTalk.js index de7f26b86ab..225df4e1e2a 100644 --- a/src/PushToTalk.js +++ b/src/PushToTalk.js @@ -30,8 +30,19 @@ export function enable(keybinding) { } const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); - widgetMessaging.unmuteJitsiAudio(); + + // Toggle mic if in toggle mode, else just activate mic + if (SettingsStore.getValue(id).toggle) { + widgetMessaging.toggleJitsiAudio(); + } else { + widgetMessaging.unmuteJitsiAudio(); + } }, () => { + // No release functionality if toggle mode is enabled + if (!SettingsStore.getValue(id).toggle) { + return; + } + const widgetId = ActiveWidgetStore.getPersistentWidgetId(); // Only try to un/mute if jitsi is onscreen diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index 15f60887688..657368cdd9f 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -42,6 +42,7 @@ export default class VoiceSettingsTab extends React.Component { pushToTalkAscii: SettingsStore.getValue(PushToTalk.id).ascii, pushToTalkKeybinding: SettingsStore.getValue(PushToTalk.id).keybinding, pushToTalkEnabled: SettingsStore.getValue(PushToTalk.id).enabled, + pushToTalkToggleModeEnabled: SettingsStore.getValue(PushToTalk.id).toggle, }; } @@ -186,13 +187,20 @@ export default class VoiceSettingsTab extends React.Component { this.setState({settingKeybinding: !this.state.settingKeybinding}); } + _onTogglePushToTalkToggleModeClicked = (enabled) => { + // Turns on/off toggle mode, which toggles the mic when the shortcut is pressed + const currentPTTState = SettingsStore.getValue(PushToTalk.id); + + this.setState({pushToTalkToggleModeEnabled: enabled}); + currentPTTState.toggle = enabled; + } + _onTogglePushToTalkClicked = (enabled) => { // Enable or disable push to talk functionality const currentPTTState = SettingsStore.getValue(PushToTalk.id); if (enabled) { // Enable push to talk - PlatformPeg.get().setupPTT(); this.setState({pushToTalkEnabled: true}); SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); @@ -215,11 +223,15 @@ export default class VoiceSettingsTab extends React.Component { _renderPushToTalkSettings = () => { const buttonLabel = this.state.settingKeybinding ? _t('Stop') : _t('Set'); - let setShortcut = null; + let setToggleShortcut = null; if (this.state.pushToTalkEnabled) { - setShortcut = ( + setToggleShortcut = (
    +
    + {_t("Push-to-talk shortcut toggles microphone")} + +
    {"Shortcut: " + this.state.pushToTalkAscii + " "}
    - {setShortcut} + {setToggleShortcut}
    ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 25856c779bf..3d2c059d0b5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -615,6 +615,7 @@ "Learn more about how we use analytics.": "Learn more about how we use analytics.", "Stop": "Stop", "Set": "Set", + "Push-to-talk shortcut toggles microphone": "Push-to-talk shortcut toggles microphone", "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.", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 84d3606d6c8..02b84315fe6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -105,6 +105,7 @@ export const SETTINGS = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: { enabled: false, + toggle: false, keybinding: [], ascii: 'Not set', }, From 75b7fd0e2213133632477b5595069bfcb9c5fda5 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Sun, 24 Feb 2019 18:54:02 +0000 Subject: [PATCH 12/25] UI Fixes --- .../settings/tabs/_VoiceSettingsTab.scss | 8 ++++ src/PushToTalk.js | 6 ++- .../views/elements/PersistedElement.js | 1 + .../views/settings/tabs/VoiceSettingsTab.js | 41 +++++++++---------- src/i18n/strings/en_EN.json | 1 + 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/res/css/views/settings/tabs/_VoiceSettingsTab.scss b/res/css/views/settings/tabs/_VoiceSettingsTab.scss index 5ddd57b0e29..a33499f9374 100644 --- a/res/css/views/settings/tabs/_VoiceSettingsTab.scss +++ b/res/css/views/settings/tabs/_VoiceSettingsTab.scss @@ -23,6 +23,14 @@ limitations under the License. margin-right: 100px; // align with the rest of the fields } +.mx_VoiceSettingsTab .mx_Shortcut { + margin-left: 130px; +} + +.mx_VoiceSettingsTab .mx_ShortcutSet { + margin-right: 130px; +} + .mx_VoiceSettingsTab_missingMediaPermissions { margin-bottom: 15px; } diff --git a/src/PushToTalk.js b/src/PushToTalk.js index 225df4e1e2a..71868c15b71 100644 --- a/src/PushToTalk.js +++ b/src/PushToTalk.js @@ -20,6 +20,10 @@ import SettingsStore from './settings/SettingsStore'; export const id = "pushToTalk"; +export function startListeningKeys() { + PlatformPeg.get().startListeningKeys(); +} + export function enable(keybinding) { PlatformPeg.get().addGlobalKeybinding(id, keybinding, () => { const widgetId = ActiveWidgetStore.getPersistentWidgetId(); @@ -39,7 +43,7 @@ export function enable(keybinding) { } }, () => { // No release functionality if toggle mode is enabled - if (!SettingsStore.getValue(id).toggle) { + if (SettingsStore.getValue(id).toggle === false) { return; } diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index e221f960551..a4904309ac5 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -118,6 +118,7 @@ export default class PersistedElement extends React.Component { // TODO: This seems quite hacky - is there a better way to // check if this is a Jitsi vs. StickerPicker widget? if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { + PushToTalk.startListeningKeys(); PushToTalk.enable(SettingsStore.getValue(PushToTalk.id).keybinding); } diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index 657368cdd9f..a005ff11ee7 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -86,6 +86,7 @@ export default class VoiceSettingsTab extends React.Component { // Used for displaying ascii-representation of current keys // in the UI listenKeydown = (event) => { + console.log("listenKeydown") const key = self._translateKeybinding(event.code); const index = keyAscii.indexOf(key); if (index === -1) { @@ -96,6 +97,7 @@ export default class VoiceSettingsTab extends React.Component { }; listenKeyup = (event) => { + console.log("listenKeyup") const index = keyAscii.indexOf(self._translateKeybinding(event.code)); if (index !== -1) { keyAscii.splice(index, 1); @@ -199,23 +201,16 @@ export default class VoiceSettingsTab extends React.Component { // Enable or disable push to talk functionality const currentPTTState = SettingsStore.getValue(PushToTalk.id); - if (enabled) { - // Enable push to talk + // Set push to talk state in SettingsStore + currentPTTState.enabled = enabled; + SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); - this.setState({pushToTalkEnabled: true}); - SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); - currentPTTState.enabled = true; + this.setState({pushToTalkEnabled: enabled}); + // Enable/disable push to talk + if (enabled) { PushToTalk.enable(currentPTTState.keybinding); - PushToTalk.setKeybinding(SettingsStore.getValue(PushToTalk.id).keybinding); } else { - // Disable push to talk - - this.setState({pushToTalkEnabled: false}); - currentPTTState.enabled = false; - SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); - this.setState({pushToTalkKeybinding: []}); - PushToTalk.disable(); } } @@ -232,14 +227,18 @@ export default class VoiceSettingsTab extends React.Component { {_t("Push-to-talk shortcut toggles microphone")}
    - {"Shortcut: " + this.state.pushToTalkAscii + " "} - - - +
    + + {_t("Shortcut:") + " " + this.state.pushToTalkAscii + " "} + + + + +
    ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3d2c059d0b5..026e0f36a0f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -616,6 +616,7 @@ "Stop": "Stop", "Set": "Set", "Push-to-talk shortcut toggles microphone": "Push-to-talk shortcut toggles microphone", + "Shortcut:": "Shortcut:", "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.", From 6050c95d0c53fb5f1853bb87ada8306d939f196c Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Sun, 24 Feb 2019 20:55:24 +0000 Subject: [PATCH 13/25] Cleanup, and ability to change shortcut during call --- src/PushToTalk.js | 28 +++++++++++++++---- src/WidgetMessaging.js | 2 +- .../views/elements/PersistedElement.js | 7 +++-- .../views/settings/tabs/VoiceSettingsTab.js | 23 +++++++-------- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/PushToTalk.js b/src/PushToTalk.js index 71868c15b71..2fc8327b1cf 100644 --- a/src/PushToTalk.js +++ b/src/PushToTalk.js @@ -16,16 +16,28 @@ limitations under the License. import PlatformPeg from './PlatformPeg'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; -import SettingsStore from './settings/SettingsStore'; +import SettingsStore, { SettingLevel } from './settings/SettingsStore'; export const id = "pushToTalk"; -export function startListeningKeys() { - PlatformPeg.get().startListeningKeys(); +export function start(keybinding) { + // In a call, start listening for keys + const currentPTTState = SettingsStore.getValue(id); + currentPTTState.isInCall = true; + SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); + setKeybinding(keybinding); +} + +export function stop() { + const currentPTTState = SettingsStore.getValue(id); + currentPTTState.isInCall = false; + SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); + removeKeybinding(); } -export function enable(keybinding) { +export function setKeybinding(keybinding) { PlatformPeg.get().addGlobalKeybinding(id, keybinding, () => { + console.warn("Keybinding pressed") const widgetId = ActiveWidgetStore.getPersistentWidgetId(); // Only try to un/mute if jitsi is onscreen @@ -37,13 +49,15 @@ export function enable(keybinding) { // Toggle mic if in toggle mode, else just activate mic if (SettingsStore.getValue(id).toggle) { + console.warn("Toggling") widgetMessaging.toggleJitsiAudio(); } else { + console.warn("normal") widgetMessaging.unmuteJitsiAudio(); } }, () => { // No release functionality if toggle mode is enabled - if (SettingsStore.getValue(id).toggle === false) { + if (SettingsStore.getValue(id).toggle === true) { return; } @@ -57,9 +71,11 @@ export function enable(keybinding) { const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); widgetMessaging.muteJitsiAudio(); }); + PlatformPeg.get().startListeningKeys(); } -export function disable() { +export function removeKeybinding() { const keybinding = SettingsStore.getValue(id).keybinding; PlatformPeg.get().removeGlobalKeybinding(id, keybinding); + PlatformPeg.get().stopListeningKeys(); } diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index f8c6961f9d8..13686309098 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -103,7 +103,7 @@ export default class WidgetMessaging { toggleJitsiAudio() { return this.messageToWidget({ api: OUTBOUND_API_NAME, - action: "audioMuteToggle", + action: "audioToggle", }).then((response) => { return response.success; }); diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index a4904309ac5..ac7ac71f2f7 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -118,8 +118,8 @@ export default class PersistedElement extends React.Component { // TODO: This seems quite hacky - is there a better way to // check if this is a Jitsi vs. StickerPicker widget? if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { - PushToTalk.startListeningKeys(); - PushToTalk.enable(SettingsStore.getValue(PushToTalk.id).keybinding); + console.warn("JITSI MOUNTING") + PushToTalk.start(SettingsStore.getValue(PushToTalk.id).keybinding); } this.updateChild(); @@ -137,7 +137,8 @@ export default class PersistedElement extends React.Component { // Stop Push-To-Talk service when Jitsi widget is unmounted if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { - PushToTalk.disable(); + console.warn("JITSI UNMOUNTING") + PushToTalk.stop(); } } diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index a005ff11ee7..da9502f3c56 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -55,6 +55,7 @@ export default class VoiceSettingsTab extends React.Component { //ipcRenderer.get().removeListener('settings', this._electronSettings); // Stop recording push-to-talk shortcut if Settings or tab is closed + console.log("Stop recording!") this._stopRecordingGlobalShortcut(); } } @@ -166,13 +167,17 @@ export default class VoiceSettingsTab extends React.Component { // Start listening for keypresses and show current held shortcut on // screen + PushToTalk.removeKeybinding(); + this.state.pushToTalkAscii = 'Press Keys'; this._startRecordingGlobalShortcut('pushToTalkAscii', 'pushToTalkKeybinding'); } else { this._stopRecordingGlobalShortcut(); - // Disable and unregister old shortcut - PushToTalk.disable(); + if (currentPTTState.isInCall) { + // Switch shortcut while in a call + PushToTalk.setKeybinding(this.state.pushToTalkKeybinding); + } // Set the keybinding they've currently selected currentPTTState.keybinding = this.state.pushToTalkKeybinding; @@ -180,9 +185,6 @@ export default class VoiceSettingsTab extends React.Component { // Update push to talk keybinding SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); - - // Enable and register new shortcut - PushToTalk.enable(currentPTTState.keybinding); } // Toggle setting state @@ -191,10 +193,12 @@ export default class VoiceSettingsTab extends React.Component { _onTogglePushToTalkToggleModeClicked = (enabled) => { // Turns on/off toggle mode, which toggles the mic when the shortcut is pressed + const currentPTTState = SettingsStore.getValue(PushToTalk.id); + currentPTTState.toggle = enabled; + SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); this.setState({pushToTalkToggleModeEnabled: enabled}); - currentPTTState.toggle = enabled; } _onTogglePushToTalkClicked = (enabled) => { @@ -206,13 +210,6 @@ export default class VoiceSettingsTab extends React.Component { SettingsStore.setValue(PushToTalk.id, null, SettingLevel.DEVICE, currentPTTState); this.setState({pushToTalkEnabled: enabled}); - - // Enable/disable push to talk - if (enabled) { - PushToTalk.enable(currentPTTState.keybinding); - } else { - PushToTalk.disable(); - } } _renderPushToTalkSettings = () => { From d346c135822d854f0dff00c995b901448de20b92 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Sun, 24 Feb 2019 20:58:03 +0000 Subject: [PATCH 14/25] Update comments --- src/PushToTalk.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PushToTalk.js b/src/PushToTalk.js index 2fc8327b1cf..56c2eaba51f 100644 --- a/src/PushToTalk.js +++ b/src/PushToTalk.js @@ -21,7 +21,7 @@ import SettingsStore, { SettingLevel } from './settings/SettingsStore'; export const id = "pushToTalk"; export function start(keybinding) { - // In a call, start listening for keys + // Call starting, start listening for keys const currentPTTState = SettingsStore.getValue(id); currentPTTState.isInCall = true; SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); @@ -29,6 +29,7 @@ export function start(keybinding) { } export function stop() { + // Call finished, stop listening for keys const currentPTTState = SettingsStore.getValue(id); currentPTTState.isInCall = false; SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); @@ -36,8 +37,8 @@ export function stop() { } export function setKeybinding(keybinding) { + // Add keybinding listener PlatformPeg.get().addGlobalKeybinding(id, keybinding, () => { - console.warn("Keybinding pressed") const widgetId = ActiveWidgetStore.getPersistentWidgetId(); // Only try to un/mute if jitsi is onscreen @@ -49,10 +50,8 @@ export function setKeybinding(keybinding) { // Toggle mic if in toggle mode, else just activate mic if (SettingsStore.getValue(id).toggle) { - console.warn("Toggling") widgetMessaging.toggleJitsiAudio(); } else { - console.warn("normal") widgetMessaging.unmuteJitsiAudio(); } }, () => { @@ -75,6 +74,7 @@ export function setKeybinding(keybinding) { } export function removeKeybinding() { + // Remove keybinding listener const keybinding = SettingsStore.getValue(id).keybinding; PlatformPeg.get().removeGlobalKeybinding(id, keybinding); PlatformPeg.get().stopListeningKeys(); From 2e739cfbecd3be143c09daff86a23323329272cc Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Sun, 24 Feb 2019 21:12:14 +0000 Subject: [PATCH 15/25] lint --- src/components/views/elements/PersistedElement.js | 2 -- src/components/views/settings/tabs/VoiceSettingsTab.js | 3 --- 2 files changed, 5 deletions(-) diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index ac7ac71f2f7..5db9119c48d 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -118,7 +118,6 @@ export default class PersistedElement extends React.Component { // TODO: This seems quite hacky - is there a better way to // check if this is a Jitsi vs. StickerPicker widget? if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { - console.warn("JITSI MOUNTING") PushToTalk.start(SettingsStore.getValue(PushToTalk.id).keybinding); } @@ -137,7 +136,6 @@ export default class PersistedElement extends React.Component { // Stop Push-To-Talk service when Jitsi widget is unmounted if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { - console.warn("JITSI UNMOUNTING") PushToTalk.stop(); } } diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index da9502f3c56..d1486e9311e 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -55,7 +55,6 @@ export default class VoiceSettingsTab extends React.Component { //ipcRenderer.get().removeListener('settings', this._electronSettings); // Stop recording push-to-talk shortcut if Settings or tab is closed - console.log("Stop recording!") this._stopRecordingGlobalShortcut(); } } @@ -87,7 +86,6 @@ export default class VoiceSettingsTab extends React.Component { // Used for displaying ascii-representation of current keys // in the UI listenKeydown = (event) => { - console.log("listenKeydown") const key = self._translateKeybinding(event.code); const index = keyAscii.indexOf(key); if (index === -1) { @@ -98,7 +96,6 @@ export default class VoiceSettingsTab extends React.Component { }; listenKeyup = (event) => { - console.log("listenKeyup") const index = keyAscii.indexOf(self._translateKeybinding(event.code)); if (index !== -1) { keyAscii.splice(index, 1); From 52348f7ffb0b00f88c49024114ac4420df330f76 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Sun, 24 Feb 2019 21:12:55 +0000 Subject: [PATCH 16/25] lint --- src/components/views/settings/tabs/VoiceSettingsTab.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index d1486e9311e..ef32579409c 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -265,7 +265,9 @@ export default class VoiceSettingsTab extends React.Component { _requestMediaPermissions = () => { const getUserMedia = ( - window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mediaDevices.mozGetUserMedia + window.navigator.getUserMedia || + window.navigator.webkitGetUserMedia || + window.navigator.mediaDevices.mozGetUserMedia ); if (getUserMedia) { return getUserMedia.apply(window.navigator, [ From 6ac47fb98f96718bfe85c5cfeb9c3d3536cb9ac0 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 27 Feb 2019 22:14:09 +0000 Subject: [PATCH 17/25] Fix UI and being unable to talk to widget --- .../settings/tabs/_VoiceSettingsTab.scss | 12 +++++++--- src/components/views/elements/AppTile.js | 4 +--- .../views/settings/tabs/VoiceSettingsTab.js | 22 ++++++++++--------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/res/css/views/settings/tabs/_VoiceSettingsTab.scss b/res/css/views/settings/tabs/_VoiceSettingsTab.scss index a33499f9374..8dac8aa97e0 100644 --- a/res/css/views/settings/tabs/_VoiceSettingsTab.scss +++ b/res/css/views/settings/tabs/_VoiceSettingsTab.scss @@ -23,12 +23,18 @@ limitations under the License. margin-right: 100px; // align with the rest of the fields } -.mx_VoiceSettingsTab .mx_Shortcut { - margin-left: 130px; +.mx_Shortcut { + height: 30px; +} + +.mx_Shortcut_label { + margin-left: 30px; } .mx_VoiceSettingsTab .mx_ShortcutSet { - margin-right: 130px; + float: right; + width: 48px; + margin-right: 17px; } .mx_VoiceSettingsTab_missingMediaPermissions { diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index b10177386e3..31b12719e59 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -329,9 +329,7 @@ export default class AppTile extends React.Component { * Called when widget iframe has finished loading */ _onLoaded() { - if (!ActiveWidgetStore.getWidgetMessaging(this.props.id)) { - this._setupWidgetMessaging(); - } + this._setupWidgetMessaging(); ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId); this.setState({loading: false}); } diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js index ef32579409c..7b5420c9e76 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/VoiceSettingsTab.js @@ -222,16 +222,18 @@ export default class VoiceSettingsTab extends React.Component {
    - - {_t("Shortcut:") + " " + this.state.pushToTalkAscii + " "} - - - - +
    + + {_t("Shortcut:") + " " + this.state.pushToTalkAscii + " "} + + + + +
    ); From 7e8411d2b0d952cdd46fe2f0852ae8bcb5205524 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 28 Feb 2019 00:04:57 +0000 Subject: [PATCH 18/25] Fix build issues, move PushToTalk to class --- .../tabs/user/_VoiceUserSettingsTab.scss | 2 +- src/PushToTalk.js | 102 +++++++++--------- .../views/elements/PersistedElement.js | 2 +- .../tabs/user/VoiceUserSettingsTab.js | 7 +- 4 files changed, 57 insertions(+), 56 deletions(-) diff --git a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss index bbee295c81c..18bac13913b 100644 --- a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss @@ -31,7 +31,7 @@ limitations under the License. margin-left: 30px; } -.mx_VoiceSettingsTab .mx_ShortcutSet { +.mx_VoiceUserSettingsTab .mx_ShortcutSet { float: right; width: 48px; margin-right: 17px; diff --git a/src/PushToTalk.js b/src/PushToTalk.js index 56c2eaba51f..f9feb0f2252 100644 --- a/src/PushToTalk.js +++ b/src/PushToTalk.js @@ -18,64 +18,66 @@ import PlatformPeg from './PlatformPeg'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import SettingsStore, { SettingLevel } from './settings/SettingsStore'; -export const id = "pushToTalk"; +export default class PushToTalk { + static id = "pushToTalk"; -export function start(keybinding) { - // Call starting, start listening for keys - const currentPTTState = SettingsStore.getValue(id); - currentPTTState.isInCall = true; - SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); - setKeybinding(keybinding); -} + static start(keybinding) { + // Call starting, start listening for keys + const currentPTTState = SettingsStore.getValue(this.id); + currentPTTState.isInCall = true; + SettingsStore.setValue(this.id, null, SettingLevel.DEVICE, currentPTTState); + this.setKeybinding(keybinding); + } -export function stop() { - // Call finished, stop listening for keys - const currentPTTState = SettingsStore.getValue(id); - currentPTTState.isInCall = false; - SettingsStore.setValue(id, null, SettingLevel.DEVICE, currentPTTState); - removeKeybinding(); -} + static stop() { + // Call finished, stop listening for keys + const currentPTTState = SettingsStore.getValue(this.id); + currentPTTState.isInCall = false; + SettingsStore.setValue(this.id, null, SettingLevel.DEVICE, currentPTTState); + this.removeKeybinding(); + } -export function setKeybinding(keybinding) { - // Add keybinding listener - PlatformPeg.get().addGlobalKeybinding(id, keybinding, () => { - const widgetId = ActiveWidgetStore.getPersistentWidgetId(); + static setKeybinding(keybinding) { + // Add keybinding listener + PlatformPeg.get().addGlobalKeybinding(this.id, keybinding, () => { + const widgetId = ActiveWidgetStore.getPersistentWidgetId(); - // Only try to un/mute if jitsi is onscreen - if (widgetId === null || widgetId === undefined) { - return; - } + // Only try to un/mute if jitsi is onscreen + if (widgetId === null || widgetId === undefined) { + return; + } - const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); + const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); - // Toggle mic if in toggle mode, else just activate mic - if (SettingsStore.getValue(id).toggle) { - widgetMessaging.toggleJitsiAudio(); - } else { - widgetMessaging.unmuteJitsiAudio(); - } - }, () => { - // No release functionality if toggle mode is enabled - if (SettingsStore.getValue(id).toggle === true) { - return; - } + // Toggle mic if in toggle mode, else just activate mic + if (SettingsStore.getValue(this.id).toggle) { + widgetMessaging.toggleJitsiAudio(); + } else { + widgetMessaging.unmuteJitsiAudio(); + } + }, () => { + // No release functionality if toggle mode is enabled + if (SettingsStore.getValue(this.id).toggle === true) { + return; + } - const widgetId = ActiveWidgetStore.getPersistentWidgetId(); + const widgetId = ActiveWidgetStore.getPersistentWidgetId(); - // Only try to un/mute if jitsi is onscreen - if (widgetId === null || widgetId === undefined) { - return; - } + // Only try to un/mute if jitsi is onscreen + if (widgetId === null || widgetId === undefined) { + return; + } - const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); - widgetMessaging.muteJitsiAudio(); - }); - PlatformPeg.get().startListeningKeys(); -} + const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(widgetId); + widgetMessaging.muteJitsiAudio(); + }); + PlatformPeg.get().startListeningKeys(); + } -export function removeKeybinding() { - // Remove keybinding listener - const keybinding = SettingsStore.getValue(id).keybinding; - PlatformPeg.get().removeGlobalKeybinding(id, keybinding); - PlatformPeg.get().stopListeningKeys(); + static removeKeybinding() { + // Remove keybinding listener + const keybinding = SettingsStore.getValue(this.id).keybinding; + PlatformPeg.get().removeGlobalKeybinding(this.id, keybinding); + PlatformPeg.get().stopListeningKeys(); + } } diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index 5db9119c48d..a859f7b9275 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -20,7 +20,7 @@ import PropTypes from 'prop-types'; import ResizeObserver from 'resize-observer-polyfill'; import SettingsStore from '../../../settings/SettingsStore'; -import * as PushToTalk from '../../../PushToTalk'; +import PushToTalk from '../../../PushToTalk'; import dis from '../../../dispatcher'; diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index 582bd119f79..648c107c1c5 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -19,18 +19,17 @@ import {_t} from "../../../../../languageHandler"; import CallMediaHandler from "../../../../../CallMediaHandler"; import Field from "../../../elements/Field"; import AccessibleButton from "../../../elements/AccessibleButton"; -import ToggleSwitch from "../../elements/ToggleSwitch"; +import ToggleSwitch from "../../../elements/ToggleSwitch"; import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; -import PushToTalk from "../../../../PushToTalk"; +import PushToTalk from "../../../../../PushToTalk"; const Modal = require("../../../../../Modal"); const sdk = require("../../../../.."); const MatrixClientPeg = require("../../../../../MatrixClientPeg"); -const PlatformPeg = require("../../../../PlatformPeg"); +const PlatformPeg = require("../../../../../PlatformPeg"); let listenKeydown = null; let listenKeyup = null; -.mx_VoiceUserSettingsTab_missingMediaPermissions { export default class VoiceUserSettingsTab extends React.Component { constructor() { super(); From 5cca1b407997c414f4fe09e1a5c0857602906679 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 7 Mar 2019 16:21:53 +0000 Subject: [PATCH 19/25] Prevent erroneously setting state outside of settings window --- .../views/settings/tabs/user/VoiceUserSettingsTab.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index 648c107c1c5..3057f76d2b0 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -27,8 +27,12 @@ const sdk = require("../../../../.."); const MatrixClientPeg = require("../../../../../MatrixClientPeg"); const PlatformPeg = require("../../../../../PlatformPeg"); +// When removing a listener, you must specify the same function as when you added it +// Thus we save the listener functions in a global variable to access later when +// decommissioning them let listenKeydown = null; let listenKeyup = null; +let globalListenKeydown = null; export default class VoiceUserSettingsTab extends React.Component { constructor() { @@ -117,7 +121,7 @@ export default class VoiceUserSettingsTab extends React.Component { // This enables a nicer shortcut-recording experience, as the user can // press down their desired keys, release them, and then save the // shortcut without all the keys disappearing - PlatformPeg.get().onKeypress(this, (ev, event) => { + globalListenKeydown = (ev, event) => { if (event.keydown) { const index = keyCodes.indexOf(event.keycode); if (index === -1) { @@ -133,7 +137,8 @@ export default class VoiceUserSettingsTab extends React.Component { keyCodes.splice(index, 1); } } - }); + }; + PlatformPeg.get().onKeypress(this, globalListenKeydown); // Stop recording shortcut if window loses focus PlatformPeg.get().onWindowBlurred(() => { @@ -151,6 +156,7 @@ export default class VoiceUserSettingsTab extends React.Component { // Stop recording iohook keypresses PlatformPeg.get().stopListeningKeys(); + PlatformPeg.get().removeOnKeypress(this, globalListenKeydown); this.setState({settingKeybinding: false}); } From 891554cfbabcf3bb4ea8a292caec71b4d04dc6c3 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 8 Mar 2019 11:07:50 +0000 Subject: [PATCH 20/25] Change copyright and comment --- src/PushToTalk.js | 2 +- src/components/views/elements/PersistedElement.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PushToTalk.js b/src/PushToTalk.js index f9feb0f2252..6e2dd563607 100644 --- a/src/PushToTalk.js +++ b/src/PushToTalk.js @@ -1,5 +1,5 @@ /* -Copyright 2018 Andrew Morgan +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. diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index a859f7b9275..8189bfdc37f 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -115,8 +115,6 @@ export default class PersistedElement extends React.Component { componentDidMount() { // Start Push-To-Talk service when Jitsi widget is mounted - // TODO: This seems quite hacky - is there a better way to - // check if this is a Jitsi vs. StickerPicker widget? if (this.props.persistKey.includes('jitsi') && SettingsStore.getValue(PushToTalk.id).enabled) { PushToTalk.start(SettingsStore.getValue(PushToTalk.id).keybinding); } From af78fa10a9bf4ede74c8fa0ba7fbaa84512576ec Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 8 Mar 2019 11:08:48 +0000 Subject: [PATCH 21/25] Fix custom key translations --- src/components/views/settings/tabs/user/VoiceUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index 3057f76d2b0..ab568fcc957 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -72,7 +72,7 @@ export default class VoiceUserSettingsTab extends React.Component { // Custom translations to make keycodes look nicer const translations = { "ShiftLeft": "Shift", - "AltLeft": "Shift", + "AltLeft": "Alt", }; if (key in translations) { key = translations[key]; From 355d490da1349c98f4930239cf114199869d570f Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 8 Mar 2019 11:12:03 +0000 Subject: [PATCH 22/25] Remove dead code and fix translation --- .../views/settings/tabs/user/VoiceUserSettingsTab.js | 4 +--- src/i18n/strings/en_EN.json | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index ab568fcc957..7be8f535bc6 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -56,8 +56,6 @@ export default class VoiceUserSettingsTab extends React.Component { componentWillUnmount(): void { if (PlatformPeg.get().supportsAutoLaunch()) { - //ipcRenderer.get().removeListener('settings', this._electronSettings); - // Stop recording push-to-talk shortcut if Settings or tab is closed this._stopRecordingGlobalShortcut(); } @@ -230,7 +228,7 @@ export default class VoiceUserSettingsTab extends React.Component {
    - {_t("Shortcut:") + " " + this.state.pushToTalkAscii + " "} + {_t("Shortcut") + ": " + this.state.pushToTalkAscii + " "}