Merge branch 'master' into webrtc

This commit is contained in:
Jonas Herzig 2020-11-25 14:11:03 +01:00
commit 36801fb641
8 changed files with 1547 additions and 1725 deletions

View file

@ -36,6 +36,8 @@ window.mumbleWebConfig = {
'matrix': false, // enable Matrix Widget support (mostly auto-detected; implies 'joinDialog')
'avatarurl': '', // download and set the user's Mumble avatar to the image at this URL
// General
'theme': 'MetroMumbleLight'
'theme': 'MetroMumbleLight',
'startMute': false,
'startDeaf': false
}
}

View file

@ -2,6 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Favicon as generated by realfavicongenerator.net (slightly modified for webpack) -->
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" href="favicon/favicon-32x32.png" sizes="32x32">
@ -77,6 +78,22 @@
</form>
</div>
<!-- /ko -->
<!-- ko with: addChannelDialog -->
<div class="add-channel-dialog dialog" data-bind="visible: visible()">
<div class="dialog-header">
Add channel
</div>
<form data-bind="submit: addchannel">
<table>
<tr>
<td>Channel</td>
<td><input id="channelName" type="text" data-bind="value: channelName" required></td>
</tr>
</table>
<input class="dialog-submit" type="submit" value="Add channel">
</form>
</div>
<!-- /ko -->
<!-- ko with: connectDialog -->
<div class="join-dialog dialog" data-bind="visible: visible() && joinOnly()">
<div class="dialog-header">
@ -412,7 +429,7 @@
class="join">
Join Channel
</li>
<li data-bind="css: { disabled: !canAdd() }"
<li data-bind="css: { disabled: !canAdd() }, click: $root.openAddChannel.bind($root, $root.thisUser())"
class="add">
Add
</li>
@ -420,7 +437,7 @@
class="edit">
Edit
</li>
<li data-bind="css: { disabled: !canRemove() }"
<li data-bind="css: { disabled: !canRemove() }, click: $root.ChannelRemove.bind($root, $root.thisUser())"
class="remove">
Remove
</li>
@ -462,40 +479,52 @@
</script>
<div class="toolbar" data-bind="css: { 'toolbar-horizontal': toolbarHorizontal(),
'toolbar-vertical': !toolbarHorizontal() }">
<img class="handle-horizontal" src="/svg/handle_horizontal.svg"
data-bind="click: toggleToolbarOrientation">
<img class="handle-vertical" src="/svg/handle_vertical.svg"
data-bind="click: toggleToolbarOrientation">
<img class="tb-horizontal handle-horizontal" src="/svg/handle_horizontal.svg"
data-bind="click: toggleToolbarOrientation"
title="Switch Orientation" alt="Switch Orientation">
<img class="tb-vertical handle-vertical" src="/svg/handle_vertical.svg"
data-bind="click: toggleToolbarOrientation"
title="Switch Orientation" alt="Switch Orientation">
<img class="tb-connect" data-bind="visible: !connectDialog.joinOnly(),
click: connectDialog.show"
rel="connect" src="/svg/applications-internet.svg">
rel="connect" src="/svg/applications-internet.svg"
title="Connect to Server" alt="Connection">
<img class="tb-information" rel="information" src="/svg/information_icon.svg"
data-bind="click: connectionInfo.show,
css: { disabled: !thisUser() }">
css: { disabled: !thisUser() }"
title="Information" alt="Information">
<div class="divider"></div>
<img class="tb-mute" data-bind="visible: !selfMute(),
click: function () { requestMute(thisUser()) }"
rel="mute" src="/svg/audio-input-microphone.svg">
rel="mute" src="/svg/audio-input-microphone.svg"
title="Mute" alt="Mute">
<img class="tb-unmute tb-active" data-bind="visible: selfMute,
click: function () { requestUnmute(thisUser()) }"
rel="unmute" src="/svg/audio-input-microphone-muted.svg">
rel="unmute" src="/svg/audio-input-microphone-muted.svg"
title="Unmute" alt="Unmute">
<img class="tb-deaf" data-bind="visible: !selfDeaf(),
click: function () { requestDeaf(thisUser()) }"
rel="deaf" src="/svg/audio-output.svg">
rel="deaf" src="/svg/audio-output.svg"
title="Deafen" alt="Deafen">
<img class="tb-undeaf tb-active" data-bind="visible: selfDeaf,
click: function () { requestUndeaf(thisUser()) }"
rel="undeaf" src="/svg/audio-output-deafened.svg">
rel="undeaf" src="/svg/audio-output-deafened.svg"
title="Undeafen" alt="Undeafen">
<img class="tb-record" data-bind="click: function(){}"
rel="record" src="/svg/media-record.svg">
rel="record" src="/svg/media-record.svg"
title="Record" alt="Record">
<div class="divider"></div>
<img class="tb-comment" data-bind="click: commentDialog.show"
rel="comment" src="/svg/toolbar-comment.svg">
rel="comment" src="/svg/toolbar-comment.svg"
title="Comment" alt="Comment">
<div class="divider"></div>
<img class="tb-settings" data-bind="click: openSettings"
rel="settings" src="/svg/config_basic.svg">
rel="settings" src="/svg/config_basic.svg"
title="Settings" alt="Settings">
<div class="divider"></div>
<img class="tb-sourcecode" data-bind="click: openSourceCode"
rel="Source Code" src="/svg/source-code.svg">
rel="Source Code" src="/svg/source-code.svg"
title="Open Soure Code" alt="Open Source Code">
</div>
<div class="chat">
<script type="text/html" id="log-generic">
@ -533,8 +562,10 @@
</div>
</div>
<form data-bind="submit: submitMessageBox">
<input id="message-box" type="text" data-bind="
attr: { placeholder: messageBoxHint }, textInput: messageBox">
<textarea id="message-box" row=1 data-bind="
attr: { placeholder: messageBoxHint },
textInput: messageBox,
event: {keypress: submitOnEnter}"></textarea>
</form>
</div>
<script type="text/html" id="channel">

View file

@ -7,18 +7,57 @@ import audioContext from 'audio-context'
import ko from 'knockout'
import _dompurify from 'dompurify'
import keyboardjs from 'keyboardjs'
import anchorme from 'anchorme'
import { ContinuousVoiceHandler, PushToTalkVoiceHandler, VADVoiceHandler, initVoice } from './voice'
import {initialize as localizationInitialize, translate} from './loc';
const dompurify = _dompurify(window)
// from: https://gist.github.com/haliphax/5379454
ko.extenders.scrollFollow = function (target, selector) {
target.subscribe(function (chat) {
const el = document.querySelector(selector);
// the scroll bar is all the way down, so we know they want to follow the text
if (el.scrollTop == el.scrollHeight - el.clientHeight) {
// have to push our code outside of this thread since the text hasn't updated yet
setTimeout(function () { el.scrollTop = el.scrollHeight - el.clientHeight; }, 0);
} else {
// send notification
const last = chat[chat.length - 1]
if (Notification.permission == 'granted' && last.type != 'chat-message-self') {
let sender = 'Mumble Server'
if (last.user && last.user.name) sender=last.user.name()
new Notification(sender, {body: dompurify.sanitize(last.message, {ALLOWED_TAGS:[]})})
}
}
});
return target;
};
function sanitize (html) {
return dompurify.sanitize(html, {
ALLOWED_TAGS: ['br', 'b', 'i', 'u', 'a', 'span', 'p']
ALLOWED_TAGS: ['br', 'b', 'i', 'u', 'a', 'span', 'p', 'img', 'center']
})
}
const anchormeOptions = {
// force target _blank attribute
attributes: {
target: "_blank"
},
// force https protocol except email
protocol: function(s) {
if (anchorme.validate.email(s)) {
return "mailto:";
} else {
return "https://";
}
}
}
function openContextMenu (event, contextMenu, target) {
contextMenu.posX(event.clientX)
contextMenu.posY(event.clientY)
@ -47,6 +86,20 @@ function ContextMenu () {
self.target = ko.observable()
}
function AddChannelDialog () {
var self = this;
self.channelName = ko.observable('')
self.parentID = 0;
self.visible = ko.observable(false);
self.show = self.visible.bind(self.visible, true)
self.hide = self.visible.bind(self.visible, false)
self.addchannel = function() {
self.hide();
ui.addchannel(self.channelName());
}
}
function ConnectDialog () {
var self = this
self.address = ko.observable('')
@ -286,6 +339,7 @@ class GlobalBindings {
this.userContextMenu = new ContextMenu()
this.channelContextMenu = new ContextMenu()
this.connectDialog = new ConnectDialog()
this.addChannelDialog = new AddChannelDialog()
this.connectErrorDialog = new ConnectErrorDialog(this.connectDialog)
this.connectionInfo = new ConnectionInfo(this)
this.commentDialog = new CommentDialog()
@ -300,8 +354,8 @@ class GlobalBindings {
this.messageBox = ko.observable('')
this.toolbarHorizontal = ko.observable(!this.settings.toolbarVertical)
this.selected = ko.observable()
this.selfMute = ko.observable()
this.selfDeaf = ko.observable()
this.selfMute = ko.observable(this.config.defaults.startMute)
this.selfDeaf = ko.observable(this.config.defaults.startDeaf)
this.selfMute.subscribe(mute => {
if (voiceHandler) {
@ -309,6 +363,14 @@ class GlobalBindings {
}
})
this.submitOnEnter = function(data, e) {
if (e.which == 13 && !e.shiftKey) {
this.submitMessageBox();
return false;
}
return true;
}
this.toggleToolbarOrientation = () => {
this.toolbarHorizontal(!this.toolbarHorizontal())
this.settings.toolbarVertical = !this.toolbarHorizontal()
@ -323,6 +385,32 @@ class GlobalBindings {
this.settingsDialog(new SettingsDialog(this.settings))
}
this.openAddChannel = (user, channel) => {
this.addChannelDialog.parentID = channel.model._id;
this.addChannelDialog.show()
}
this.addchannel = (channelName) => {
var msg = {
name: 'ChannelState',
payload: {
parent: this.addChannelDialog.parentID || 0,
name: channelName
}
}
this.client._send(msg);
}
this.ChannelRemove = (user, channel) => {
var msg = {
name: 'ChannelRemove',
payload: {
channel_id: channel.model._id
}
}
this.client._send(msg);
}
this.applySettings = () => {
const settingsDialog = this.settingsDialog()
@ -342,10 +430,14 @@ class GlobalBindings {
}
this.getTimeString = () => {
return '[' + new Date().toLocaleTimeString('en-US') + ']'
return '[' + new Date().toLocaleTimeString(navigator.language) + ']'
}
this.connect = (username, host, port, tokens = [], password, channelName = "") => {
// if browser support Notification request permission
if ('Notification' in window) Notification.requestPermission()
this.resetClient()
this.remoteHost(host)
@ -412,7 +504,7 @@ class GlobalBindings {
type: 'chat-message',
user: sender.__ui,
channel: channels.length > 0,
message: sanitize(message)
message: anchorme({input: sanitize(message), options: anchormeOptions})
})
})
@ -639,13 +731,13 @@ class GlobalBindings {
return true // TODO check for perms
}
ui.canAdd = () => {
return false // TODO check for perms and implement
return true // TODO check for perms
}
ui.canEdit = () => {
return false // TODO check for perms and implement
}
ui.canRemove = () => {
return false // TODO check for perms and implement
return true // TODO check for perms
}
ui.canLink = () => {
return false // TODO check for perms and implement
@ -784,18 +876,23 @@ class GlobalBindings {
if (target === this.thisUser()) {
target = target.channel()
}
// Avoid blank message
if (sanitize(message).trim().length == 0) return;
// Support multiline
message = message.replace(/\n\n+/g,"\n\n");
message = message.replace(/\n/g,"<br>");
// Send message
target.model.sendMessage(message)
target.model.sendMessage(anchorme(message))
if (target.users) { // Channel
this.log.push({
type: 'chat-message-self',
message: sanitize(message),
message: anchorme({input: sanitize(message), options: anchormeOptions}),
channel: target
})
} else { // User
this.log.push({
type: 'chat-message-self',
message: sanitize(message),
message: anchorme({input: sanitize(message), options: anchormeOptions}),
user: target
})
}
@ -1134,6 +1231,31 @@ function translateEverything() {
translatePiece('.channel-context-menu .copy-mumble-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_url');
translatePiece('.channel-context-menu .copy-mumble-web-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_web_url');
translatePiece('.channel-context-menu .send-message', 'textcontent', {}, 'channelcontextmenu.send_message');
translatePiece('.toolbar .tb-horizontal', 'attribute', {'name': 'title'}, 'toolbar.orientation');
translatePiece('.toolbar .tb-horizontal', 'attribute', {'name': 'alt'}, 'toolbar.orientation');
translatePiece('.toolbar .tb-vertical', 'attribute', {'name': 'title'}, 'toolbar.orientation');
translatePiece('.toolbar .tb-vertical', 'attribute', {'name': 'alt'}, 'toolbar.orientation');
translatePiece('.toolbar .tb-connect', 'attribute', {'name': 'title'}, 'toolbar.connect');
translatePiece('.toolbar .tb-connect', 'attribute', {'name': 'alt'}, 'toolbar.connect');
translatePiece('.toolbar .tb-information', 'attribute', {'name': 'title'}, 'toolbar.information');
translatePiece('.toolbar .tb-information', 'attribute', {'name': 'alt'}, 'toolbar.information');
translatePiece('.toolbar .tb-mute', 'attribute', {'name': 'title'}, 'toolbar.mute');
translatePiece('.toolbar .tb-mute', 'attribute', {'name': 'alt'}, 'toolbar.mute');
translatePiece('.toolbar .tb-unmute', 'attribute', {'name': 'title'}, 'toolbar.unmute');
translatePiece('.toolbar .tb-unmute', 'attribute', {'name': 'alt'}, 'toolbar.unmute');
translatePiece('.toolbar .tb-deaf', 'attribute', {'name': 'title'}, 'toolbar.deaf');
translatePiece('.toolbar .tb-deaf', 'attribute', {'name': 'alt'}, 'toolbar.deaf');
translatePiece('.toolbar .tb-undeaf', 'attribute', {'name': 'title'}, 'toolbar.undeaf');
translatePiece('.toolbar .tb-undeaf', 'attribute', {'name': 'alt'}, 'toolbar.undeaf');
translatePiece('.toolbar .tb-record', 'attribute', {'name': 'title'}, 'toolbar.record');
translatePiece('.toolbar .tb-record', 'attribute', {'name': 'alt'}, 'toolbar.record');
translatePiece('.toolbar .tb-comment', 'attribute', {'name': 'title'}, 'toolbar.comment');
translatePiece('.toolbar .tb-comment', 'attribute', {'name': 'alt'}, 'toolbar.comment');
translatePiece('.toolbar .tb-settings', 'attribute', {'name': 'title'}, 'toolbar.settings');
translatePiece('.toolbar .tb-settings', 'attribute', {'name': 'alt'}, 'toolbar.settings');
translatePiece('.toolbar .tb-sourcecode', 'attribute', {'name': 'title'}, 'toolbar.sourcecode');
translatePiece('.toolbar .tb-sourcecode', 'attribute', {'name': 'alt'}, 'toolbar.sourcecode');
}
async function main() {

View file

@ -138,6 +138,7 @@ class WorkerBasedMumbleClient extends EventEmitter {
connector._addCall(this, 'setSelfMute', id)
connector._addCall(this, 'setSelfTexture', id)
connector._addCall(this, 'setAudioQuality', id)
connector._addCall(this, '_send', id)
connector._addCall(this, 'disconnect', id)
let _disconnect = this.disconnect

View file

@ -31,6 +31,19 @@
"title": "Mumble Voice Conference",
"connect": "Join Conference"
},
"toolbar": {
"orientation": "Switch Orientation",
"connect": "Connection",
"information": "Information",
"mute": "Mute",
"unmute": "Unmute",
"deaf": "Deafen",
"undeaf": "Undeafen",
"record": "Record",
"comment": "Comment",
"settings": "Settings",
"sourcecode": "Open Source Code"
},
"usercontextmenu": {
"mute": "Mute",
"deafen": "Deafen",

2968
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,40 +16,41 @@
"dist"
],
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@babel/core": "^7.12.9",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.7",
"@babel/runtime": "^7.12.5",
"anchorme": "^2.1.2",
"audio-buffer-utils": "^5.1.2",
"audio-context": "^1.0.3",
"babel-loader": "^8.1.0",
"babel-loader": "^8.2.1",
"brfs": "^2.0.2",
"bytebuffer": "^5.0.1",
"css-loader": "^3.4.2",
"dompurify": "^2.0.8",
"css-loader": "^3.6.0",
"dompurify": "^2.2.2",
"drop-stream": "^1.0.0",
"duplex-maker": "^1.0.0",
"extract-loader": "^5.0.1",
"extract-loader": "^5.1.0",
"file-loader": "^4.3.0",
"fs": "0.0.1-security",
"html-loader": "^0.5.5",
"json-loader": "^0.5.7",
"keyboardjs": "^2.5.1",
"keyboardjs": "^2.6.4",
"knockout": "^3.5.1",
"lodash.assign": "^4.2.0",
"microphone-stream": "^5.0.1",
"microphone-stream": "^5.1.0",
"mumble-client": "github:johni0702/mumble-client#f73a08b",
"mumble-client-websocket": "github:johni0702/mumble-client-websocket#5b0ed8d",
"node-sass": "^4.13.1",
"raw-loader": "^4.0.0",
"node-sass": "^4.14.1",
"raw-loader": "^4.0.2",
"regexp-replace-loader": "1.0.1",
"sass-loader": "^8.0.2",
"stream-chunker": "^1.2.8",
"to-arraybuffer": "^1.0.1",
"transform-loader": "^0.2.4",
"voice-activity-detection": "johni0702/voice-activity-detection#9f8bd90",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11",
"voice-activity-detection": "github:johni0702/voice-activity-detection#9f8bd90",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"worker-loader": "^2.0.0"
},
"optionalDependencies": {}

View file

@ -537,3 +537,49 @@ form {
.minimal .user-status {
height: 19px;
}
/* Mobile view */
@media only screen and (max-width: 600px) and (min-width: 320px) and (min-height: 600px) {
.toolbar-horizontal ~ .channel-root-container, .toolbar-vertical ~ .channel-root-container {
height:calc(100% - 440px);
position:static;
width:100%;
}
.toolbar-horizontal ~ .chat, .toolbar-vertical ~ .chat {
position:fixed;
bottom: 60px;
left:0;
width:100%;
height:330px;
y-overflow:auto;
font-size:0.8em;
z-index:10;
}
.toolbar-vertical {
flex-direction: row;
height: 36px;
margin-top: 4px;
margin-left: 1%;
padding-left: 5px;
}
#message-box {
margin: 10px 5px 10px 5px;
padding: 10px;
height: 2em;
font-size: 1.2em;
font-weight: bold;
}
.handle-vertical, .handle-horizontal {
display: none;
}
.dialog {
min-width: 350px;
}
}