Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Jitsi Push-to-Talk
Browse files Browse the repository at this point in the history
  • Loading branch information
anoadragon453 committed Nov 15, 2018
1 parent 4ea1e4e commit 3ff15cf
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 7 deletions.
50 changes: 50 additions & 0 deletions src/PushToTalk.js
Original file line number Diff line number Diff line change
@@ -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);
}
40 changes: 40 additions & 0 deletions src/WidgetMessaging.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
194 changes: 194 additions & 0 deletions src/components/structures/UserSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ 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
Expand Down Expand Up @@ -158,6 +159,9 @@ const IgnoredUser = React.createClass({
},
});

let listenKeydown;
let listenKeyup;

module.exports = React.createClass({
displayName: 'UserSettings',

Expand Down Expand Up @@ -190,6 +194,9 @@ module.exports = React.createClass({
rejectingInvites: false,
mediaDevices: null,
ignoredUsers: [],
pushToTalkAscii: SettingsStore.getValue('pushToTalk').ascii,
pushToTalkKeybinding: SettingsStore.getValue('pushToTalk').keybinding,
pushToTalkEnabled: SettingsStore.getValue('pushToTalk').enabled,
};
},

Expand Down Expand Up @@ -265,6 +272,9 @@ module.exports = React.createClass({
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();
}
},

Expand Down Expand Up @@ -655,6 +665,189 @@ module.exports = React.createClass({
</div>;
},

_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 (
<div>
<table>
<tbody>
<tr>
<td>
<input type="checkbox"
name={id}
defaultChecked={activated}
onChange={this._onTogglePushToTalkClicked}
/>
<label htmlFor={id}>{SettingsStore.getDisplayName(id)}</label>
</td>
<td>{"Shortcut: " + this.state.pushToTalkAscii}</td>
<td>
<button key={id} className="mx_Dialog_primary"
onClick={this._onSetPushToTalkClicked}
disabled={!this.state.pushToTalkEnabled}>
{buttonLabel}
</button>
</td>
</tr>
</tbody>
</table>
</div>
);
},

_renderUserInterfaceSettings: function() {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
Expand Down Expand Up @@ -1157,6 +1350,7 @@ module.exports = React.createClass({
<h3>{ _t('VoIP') }</h3>
<div className="mx_UserSettings_section">
{ WEBRTC_SETTINGS.map(this._renderDeviceSetting) }
{ PlatformPeg.get().isElectron() && this._renderPushToTalkSettings() }
{ this._renderWebRtcDeviceSettings() }
</div>
</div>;
Expand Down
17 changes: 16 additions & 1 deletion src/components/views/elements/PersistedElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
}

Expand All @@ -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) {
Expand Down Expand Up @@ -168,4 +182,5 @@ export default class PersistedElement extends React.Component {

return <div ref={this.collectChildContainer}></div>;
}

}
Loading

0 comments on commit 3ff15cf

Please sign in to comment.