Browse Source

Merge branch 'master' into webrtc

Jonas Herzig 3 years ago
parent
commit
36801fb641
8 changed files with 769 additions and 463 deletions
  1. 3 1
      app/config.js
  2. 49 18
      app/index.html
  3. 132 10
      app/index.js
  4. 1 0
      app/worker-client.js
  5. 13 0
      loc/en.json
  6. 509 419
      package-lock.json
  7. 16 15
      package.json
  8. 46 0
      themes/MetroMumbleLight/main.scss

+ 3 - 1
app/config.js

@@ -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
   }
 }

+ 49 - 18
app/index.html

@@ -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">

+ 132 - 10
app/index.js

@@ -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() {

+ 1 - 0
app/worker-client.js

@@ -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

+ 13 - 0
loc/en.json

@@ -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",

File diff suppressed because it is too large
+ 509 - 419
package-lock.json


+ 16 - 15
package.json

@@ -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": {}

+ 46 - 0
themes/MetroMumbleLight/main.scss

@@ -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;
+  }
+}

Some files were not shown because too many files changed in this diff