index.js 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. import 'stream-browserify' // see https://github.com/ericgundrum/pouch-websocket-sync-example/commit/2a4437b013092cc7b2cd84cf1499172c84a963a3
  2. import 'subworkers' // polyfill for https://bugs.chromium.org/p/chromium/issues/detail?id=31666
  3. import url from 'url'
  4. import ByteBuffer from 'bytebuffer'
  5. import MumbleClient from 'mumble-client'
  6. import WorkerBasedMumbleConnector from './worker-client'
  7. import BufferQueueNode from 'web-audio-buffer-queue'
  8. import audioContext from 'audio-context'
  9. import ko from 'knockout'
  10. import _dompurify from 'dompurify'
  11. import keyboardjs from 'keyboardjs'
  12. import { ContinuousVoiceHandler, PushToTalkVoiceHandler, VADVoiceHandler, initVoice } from './voice'
  13. const dompurify = _dompurify(window)
  14. function sanitize (html) {
  15. return dompurify.sanitize(html, {
  16. ALLOWED_TAGS: ['br', 'b', 'i', 'u', 'a', 'span', 'p']
  17. })
  18. }
  19. function openContextMenu (event, contextMenu, target) {
  20. contextMenu.posX(event.clientX)
  21. contextMenu.posY(event.clientY)
  22. contextMenu.target(target)
  23. const closeListener = (event) => {
  24. // Always close, no matter where they clicked
  25. setTimeout(() => { // delay to allow click to be actually processed
  26. contextMenu.target(null)
  27. unregister()
  28. })
  29. }
  30. const unregister = () => document.removeEventListener('click', closeListener)
  31. document.addEventListener('click', closeListener)
  32. event.stopPropagation()
  33. event.preventDefault()
  34. }
  35. // GUI
  36. function ContextMenu () {
  37. var self = this
  38. self.posX = ko.observable()
  39. self.posY = ko.observable()
  40. self.target = ko.observable()
  41. }
  42. function ConnectDialog () {
  43. var self = this
  44. self.address = ko.observable('')
  45. self.port = ko.observable('')
  46. self.tokenToAdd = ko.observable('')
  47. self.selectedTokens = ko.observableArray([])
  48. self.tokens = ko.observableArray([])
  49. self.username = ko.observable('')
  50. self.password = ko.observable('')
  51. self.channelName = ko.observable('')
  52. self.joinOnly = ko.observable(false)
  53. self.visible = ko.observable(true)
  54. self.show = self.visible.bind(self.visible, true)
  55. self.hide = self.visible.bind(self.visible, false)
  56. self.connect = function () {
  57. self.hide()
  58. ui.connect(self.username(), self.address(), self.port(), self.tokens(), self.password(), self.channelName())
  59. }
  60. self.addToken = function() {
  61. if ((self.tokenToAdd() != "") && (self.tokens.indexOf(self.tokenToAdd()) < 0)) {
  62. self.tokens.push(self.tokenToAdd())
  63. }
  64. self.tokenToAdd("")
  65. }
  66. self.removeSelectedTokens = function() {
  67. this.tokens.removeAll(this.selectedTokens())
  68. this.selectedTokens([])
  69. }
  70. }
  71. function ConnectErrorDialog (connectDialog) {
  72. var self = this
  73. self.type = ko.observable(0)
  74. self.reason = ko.observable('')
  75. self.username = connectDialog.username
  76. self.password = connectDialog.password
  77. self.joinOnly = connectDialog.joinOnly
  78. self.visible = ko.observable(false)
  79. self.show = self.visible.bind(self.visible, true)
  80. self.hide = self.visible.bind(self.visible, false)
  81. self.connect = () => {
  82. self.hide()
  83. connectDialog.connect()
  84. }
  85. }
  86. class ConnectionInfo {
  87. constructor (ui) {
  88. this._ui = ui
  89. this.visible = ko.observable(false)
  90. this.serverVersion = ko.observable()
  91. this.latencyMs = ko.observable(NaN)
  92. this.latencyDeviation = ko.observable(NaN)
  93. this.remoteHost = ko.observable()
  94. this.remotePort = ko.observable()
  95. this.maxBitrate = ko.observable(NaN)
  96. this.currentBitrate = ko.observable(NaN)
  97. this.maxBandwidth = ko.observable(NaN)
  98. this.currentBandwidth = ko.observable(NaN)
  99. this.codec = ko.observable()
  100. this.show = () => {
  101. if (!ui.thisUser()) return
  102. this.update()
  103. this.visible(true)
  104. }
  105. this.hide = () => this.visible(false)
  106. }
  107. update () {
  108. let client = this._ui.client
  109. this.serverVersion(client.serverVersion)
  110. let dataStats = client.dataStats
  111. if (dataStats) {
  112. this.latencyMs(dataStats.mean)
  113. this.latencyDeviation(Math.sqrt(dataStats.variance))
  114. }
  115. this.remoteHost(this._ui.remoteHost())
  116. this.remotePort(this._ui.remotePort())
  117. let spp = this._ui.settings.samplesPerPacket
  118. let maxBitrate = client.getMaxBitrate(spp, false)
  119. let maxBandwidth = client.maxBandwidth
  120. let actualBitrate = client.getActualBitrate(spp, false)
  121. let actualBandwidth = MumbleClient.calcEnforcableBandwidth(actualBitrate, spp, false)
  122. this.maxBitrate(maxBitrate)
  123. this.currentBitrate(actualBitrate)
  124. this.maxBandwidth(maxBandwidth)
  125. this.currentBandwidth(actualBandwidth)
  126. this.codec('Opus') // only one supported for sending
  127. }
  128. }
  129. function CommentDialog () {
  130. var self = this
  131. self.visible = ko.observable(false)
  132. self.show = function () {
  133. self.visible(true)
  134. }
  135. }
  136. class SettingsDialog {
  137. constructor (settings) {
  138. this.voiceMode = ko.observable(settings.voiceMode)
  139. this.pttKey = ko.observable(settings.pttKey)
  140. this.pttKeyDisplay = ko.observable(settings.pttKey)
  141. this.vadLevel = ko.observable(settings.vadLevel)
  142. this.testVadLevel = ko.observable(0)
  143. this.testVadActive = ko.observable(false)
  144. this.showAvatars = ko.observable(settings.showAvatars())
  145. this.userCountInChannelName = ko.observable(settings.userCountInChannelName())
  146. // Need to wrap this in a pureComputed to make sure it's always numeric
  147. let audioBitrate = ko.observable(settings.audioBitrate)
  148. this.audioBitrate = ko.pureComputed({
  149. read: audioBitrate,
  150. write: (value) => audioBitrate(Number(value))
  151. })
  152. this.samplesPerPacket = ko.observable(settings.samplesPerPacket)
  153. this.msPerPacket = ko.pureComputed({
  154. read: () => this.samplesPerPacket() / 48,
  155. write: (value) => this.samplesPerPacket(value * 48)
  156. })
  157. this._setupTestVad()
  158. this.vadLevel.subscribe(() => this._setupTestVad())
  159. }
  160. _setupTestVad () {
  161. if (this._testVad) {
  162. this._testVad.end()
  163. }
  164. let dummySettings = new Settings({})
  165. this.applyTo(dummySettings)
  166. this._testVad = new VADVoiceHandler(null, dummySettings)
  167. this._testVad.on('started_talking', () => this.testVadActive(true))
  168. .on('stopped_talking', () => this.testVadActive(false))
  169. .on('level', level => this.testVadLevel(level))
  170. testVoiceHandler = this._testVad
  171. }
  172. applyTo (settings) {
  173. settings.voiceMode = this.voiceMode()
  174. settings.pttKey = this.pttKey()
  175. settings.vadLevel = this.vadLevel()
  176. settings.showAvatars(this.showAvatars())
  177. settings.userCountInChannelName(this.userCountInChannelName())
  178. settings.audioBitrate = this.audioBitrate()
  179. settings.samplesPerPacket = this.samplesPerPacket()
  180. }
  181. end () {
  182. this._testVad.end()
  183. testVoiceHandler = null
  184. }
  185. recordPttKey () {
  186. var combo = []
  187. const keydown = e => {
  188. combo = e.pressedKeys
  189. let comboStr = combo.join(' + ')
  190. this.pttKeyDisplay('> ' + comboStr + ' <')
  191. }
  192. const keyup = () => {
  193. keyboardjs.unbind('', keydown, keyup)
  194. let comboStr = combo.join(' + ')
  195. if (comboStr) {
  196. this.pttKey(comboStr).pttKeyDisplay(comboStr)
  197. } else {
  198. this.pttKeyDisplay(this.pttKey())
  199. }
  200. }
  201. keyboardjs.bind('', keydown, keyup)
  202. this.pttKeyDisplay('> ? <')
  203. }
  204. totalBandwidth () {
  205. return MumbleClient.calcEnforcableBandwidth(
  206. this.audioBitrate(),
  207. this.samplesPerPacket(),
  208. true
  209. )
  210. }
  211. positionBandwidth () {
  212. return this.totalBandwidth() - MumbleClient.calcEnforcableBandwidth(
  213. this.audioBitrate(),
  214. this.samplesPerPacket(),
  215. false
  216. )
  217. }
  218. overheadBandwidth () {
  219. return MumbleClient.calcEnforcableBandwidth(
  220. 0,
  221. this.samplesPerPacket(),
  222. false
  223. )
  224. }
  225. }
  226. class Settings {
  227. constructor (defaults) {
  228. const load = key => window.localStorage.getItem('mumble.' + key)
  229. this.voiceMode = load('voiceMode') || defaults.voiceMode
  230. this.pttKey = load('pttKey') || defaults.pttKey
  231. this.vadLevel = load('vadLevel') || defaults.vadLevel
  232. this.toolbarVertical = load('toolbarVertical') || defaults.toolbarVertical
  233. this.showAvatars = ko.observable(load('showAvatars') || defaults.showAvatars)
  234. this.userCountInChannelName = ko.observable(load('userCountInChannelName') || defaults.userCountInChannelName)
  235. this.audioBitrate = Number(load('audioBitrate')) || defaults.audioBitrate
  236. this.samplesPerPacket = Number(load('samplesPerPacket')) || defaults.samplesPerPacket
  237. }
  238. save () {
  239. const save = (key, val) => window.localStorage.setItem('mumble.' + key, val)
  240. save('voiceMode', this.voiceMode)
  241. save('pttKey', this.pttKey)
  242. save('vadLevel', this.vadLevel)
  243. save('toolbarVertical', this.toolbarVertical)
  244. save('showAvatars', this.showAvatars())
  245. save('userCountInChannelName', this.userCountInChannelName())
  246. save('audioBitrate', this.audioBitrate)
  247. save('samplesPerPacket', this.samplesPerPacket)
  248. }
  249. }
  250. class GlobalBindings {
  251. constructor (config) {
  252. this.config = config
  253. this.settings = new Settings(config.settings)
  254. this.connector = new WorkerBasedMumbleConnector()
  255. this.client = null
  256. this.userContextMenu = new ContextMenu()
  257. this.channelContextMenu = new ContextMenu()
  258. this.connectDialog = new ConnectDialog()
  259. this.connectErrorDialog = new ConnectErrorDialog(this.connectDialog)
  260. this.connectionInfo = new ConnectionInfo(this)
  261. this.commentDialog = new CommentDialog()
  262. this.settingsDialog = ko.observable()
  263. this.minimalView = ko.observable(false)
  264. this.log = ko.observableArray()
  265. this.remoteHost = ko.observable()
  266. this.remotePort = ko.observable()
  267. this.thisUser = ko.observable()
  268. this.root = ko.observable()
  269. this.avatarView = ko.observable()
  270. this.messageBox = ko.observable('')
  271. this.toolbarHorizontal = ko.observable(!this.settings.toolbarVertical)
  272. this.selected = ko.observable()
  273. this.selfMute = ko.observable()
  274. this.selfDeaf = ko.observable()
  275. this.selfMute.subscribe(mute => {
  276. if (voiceHandler) {
  277. voiceHandler.setMute(mute)
  278. }
  279. })
  280. this.toggleToolbarOrientation = () => {
  281. this.toolbarHorizontal(!this.toolbarHorizontal())
  282. this.settings.toolbarVertical = !this.toolbarHorizontal()
  283. this.settings.save()
  284. }
  285. this.select = element => {
  286. this.selected(element)
  287. }
  288. this.openSettings = () => {
  289. this.settingsDialog(new SettingsDialog(this.settings))
  290. }
  291. this.applySettings = () => {
  292. const settingsDialog = this.settingsDialog()
  293. settingsDialog.applyTo(this.settings)
  294. this._updateVoiceHandler()
  295. this.settings.save()
  296. this.closeSettings()
  297. }
  298. this.closeSettings = () => {
  299. if (this.settingsDialog()) {
  300. this.settingsDialog().end()
  301. }
  302. this.settingsDialog(null)
  303. }
  304. this.getTimeString = () => {
  305. return '[' + new Date().toLocaleTimeString('en-US') + ']'
  306. }
  307. this.connect = (username, host, port, tokens = [], password, channelName = "") => {
  308. this.resetClient()
  309. this.remoteHost(host)
  310. this.remotePort(port)
  311. log('Connecting to server ', host)
  312. // Note: This call needs to be delayed until the user has interacted with
  313. // the page in some way (which at this point they have), see: https://goo.gl/7K7WLu
  314. this.connector.setSampleRate(audioContext().sampleRate)
  315. // TODO: token
  316. this.connector.connect(`wss://${host}:${port}`, {
  317. username: username,
  318. password: password,
  319. tokens: tokens
  320. }).done(client => {
  321. log('Connected!')
  322. this.client = client
  323. // Prepare for connection errors
  324. client.on('error', (err) => {
  325. log('Connection error:', err)
  326. this.resetClient()
  327. })
  328. // Make sure we stay open if we're running as Matrix widget
  329. window.matrixWidget.setAlwaysOnScreen(true)
  330. // Register all channels, recursively
  331. if(channelName.indexOf("/") != 0) {
  332. channelName = "/"+channelName;
  333. }
  334. const registerChannel = (channel, channelPath) => {
  335. this._newChannel(channel)
  336. if(channelPath === channelName) {
  337. client.self.setChannel(channel)
  338. }
  339. channel.children.forEach(ch => registerChannel(ch, channelPath+"/"+ch.name))
  340. }
  341. registerChannel(client.root, "")
  342. // Register all users
  343. client.users.forEach(user => this._newUser(user))
  344. // Register future channels
  345. client.on('newChannel', channel => this._newChannel(channel))
  346. // Register future users
  347. client.on('newUser', user => this._newUser(user))
  348. // Handle messages
  349. client.on('message', (sender, message, users, channels, trees) => {
  350. sender = sender || { __ui: 'Server' }
  351. ui.log.push({
  352. type: 'chat-message',
  353. user: sender.__ui,
  354. channel: channels.length > 0,
  355. message: sanitize(message)
  356. })
  357. })
  358. // Set own user and root channel
  359. this.thisUser(client.self.__ui)
  360. this.root(client.root.__ui)
  361. // Upate linked channels
  362. this._updateLinks()
  363. // Log welcome message
  364. if (client.welcomeMessage) {
  365. this.log.push({
  366. type: 'welcome-message',
  367. message: sanitize(client.welcomeMessage)
  368. })
  369. }
  370. // Startup audio input processing
  371. this._updateVoiceHandler()
  372. // Tell server our mute/deaf state (if necessary)
  373. if (this.selfDeaf()) {
  374. this.client.setSelfDeaf(true)
  375. } else if (this.selfMute()) {
  376. this.client.setSelfMute(true)
  377. }
  378. }, err => {
  379. if (err.$type && err.$type.name === 'Reject') {
  380. this.connectErrorDialog.type(err.type)
  381. this.connectErrorDialog.reason(err.reason)
  382. this.connectErrorDialog.show()
  383. } else {
  384. log('Connection error:', err)
  385. }
  386. })
  387. }
  388. this._newUser = user => {
  389. const simpleProperties = {
  390. uniqueId: 'uid',
  391. username: 'name',
  392. mute: 'mute',
  393. deaf: 'deaf',
  394. suppress: 'suppress',
  395. selfMute: 'selfMute',
  396. selfDeaf: 'selfDeaf',
  397. texture: 'rawTexture',
  398. textureHash: 'textureHash',
  399. comment: 'comment'
  400. }
  401. var ui = user.__ui = {
  402. model: user,
  403. talking: ko.observable('off'),
  404. channel: ko.observable()
  405. }
  406. ui.texture = ko.pureComputed(() => {
  407. let raw = ui.rawTexture()
  408. if (!raw || raw.offset >= raw.limit) return null
  409. return 'data:image/*;base64,' + ByteBuffer.wrap(raw).toBase64()
  410. })
  411. ui.show_avatar = () => {
  412. let setting = this.settings.showAvatars()
  413. switch (setting) {
  414. case 'always':
  415. break
  416. case 'own_channel':
  417. if (this.thisUser().channel() !== ui.channel()) return false
  418. break
  419. case 'linked_channel':
  420. if (!ui.channel().linked()) return false
  421. break
  422. case 'minimal_only':
  423. if (!this.minimalView()) return false
  424. if (this.thisUser().channel() !== ui.channel()) return false
  425. break
  426. case 'never':
  427. default: return false
  428. }
  429. if (!ui.texture()) {
  430. if (ui.textureHash()) {
  431. // The user has an avatar set but it's of sufficient size to not be
  432. // included by default, so we need to fetch it explicitly now.
  433. // mumble-client should make sure we only send one request per hash
  434. user.requestTexture()
  435. }
  436. return false
  437. }
  438. return true
  439. }
  440. ui.openContextMenu = (_, event) => openContextMenu(event, this.userContextMenu, ui)
  441. ui.canChangeMute = () => {
  442. return false // TODO check for perms and implement
  443. }
  444. ui.canChangeDeafen = () => {
  445. return false // TODO check for perms and implement
  446. }
  447. ui.canChangePrioritySpeaker = () => {
  448. return false // TODO check for perms and implement
  449. }
  450. ui.canLocalMute = () => {
  451. return false // TODO implement local mute
  452. // return this.thisUser() !== ui
  453. }
  454. ui.canIgnoreMessages = () => {
  455. return false // TODO implement ignore messages
  456. // return this.thisUser() !== ui
  457. }
  458. ui.canChangeComment = () => {
  459. return false // TODO implement changing of comments
  460. // return this.thisUser() === ui // TODO check for perms
  461. }
  462. ui.canChangeAvatar = () => {
  463. return this.thisUser() === ui // TODO check for perms
  464. }
  465. ui.toggleMute = () => {
  466. if (ui.selfMute()) {
  467. this.requestUnmute(ui)
  468. } else {
  469. this.requestMute(ui)
  470. }
  471. }
  472. ui.toggleDeaf = () => {
  473. if (ui.selfDeaf()) {
  474. this.requestUndeaf(ui)
  475. } else {
  476. this.requestDeaf(ui)
  477. }
  478. }
  479. ui.viewAvatar = () => {
  480. this.avatarView(ui.texture())
  481. }
  482. ui.changeAvatar = () => {
  483. let input = document.createElement('input')
  484. input.type = 'file'
  485. input.addEventListener('change', () => {
  486. let reader = new window.FileReader()
  487. reader.onload = () => {
  488. this.client.setSelfTexture(reader.result)
  489. }
  490. reader.readAsArrayBuffer(input.files[0])
  491. })
  492. input.click()
  493. }
  494. ui.removeAvatar = () => {
  495. user.clearTexture()
  496. }
  497. Object.entries(simpleProperties).forEach(key => {
  498. ui[key[1]] = ko.observable(user[key[0]])
  499. })
  500. ui.state = ko.pureComputed(userToState, ui)
  501. if (user.channel) {
  502. ui.channel(user.channel.__ui)
  503. ui.channel().users.push(ui)
  504. ui.channel().users.sort(compareUsers)
  505. }
  506. user.on('update', (actor, properties) => {
  507. Object.entries(simpleProperties).forEach(key => {
  508. if (properties[key[0]] !== undefined) {
  509. ui[key[1]](properties[key[0]])
  510. }
  511. })
  512. if (properties.channel !== undefined) {
  513. if (ui.channel()) {
  514. ui.channel().users.remove(ui)
  515. }
  516. ui.channel(properties.channel.__ui)
  517. ui.channel().users.push(ui)
  518. ui.channel().users.sort(compareUsers)
  519. this._updateLinks()
  520. }
  521. if (properties.textureHash !== undefined) {
  522. // Invalidate avatar texture when its hash has changed
  523. // If the avatar is still visible, this will trigger a fetch of the new one.
  524. ui.rawTexture(null)
  525. }
  526. }).on('remove', () => {
  527. if (ui.channel()) {
  528. ui.channel().users.remove(ui)
  529. }
  530. }).on('voice', stream => {
  531. console.log(`User ${user.username} started takling`)
  532. var userNode = new BufferQueueNode({
  533. audioContext: audioContext()
  534. })
  535. userNode.connect(audioContext().destination)
  536. stream.on('data', data => {
  537. if (data.target === 'normal') {
  538. ui.talking('on')
  539. } else if (data.target === 'shout') {
  540. ui.talking('shout')
  541. } else if (data.target === 'whisper') {
  542. ui.talking('whisper')
  543. }
  544. userNode.write(data.buffer)
  545. }).on('end', () => {
  546. console.log(`User ${user.username} stopped takling`)
  547. ui.talking('off')
  548. userNode.end()
  549. })
  550. })
  551. }
  552. this._newChannel = channel => {
  553. const simpleProperties = {
  554. position: 'position',
  555. name: 'name',
  556. description: 'description'
  557. }
  558. var ui = channel.__ui = {
  559. model: channel,
  560. expanded: ko.observable(true),
  561. parent: ko.observable(),
  562. channels: ko.observableArray(),
  563. users: ko.observableArray(),
  564. linked: ko.observable(false)
  565. }
  566. ui.userCount = () => {
  567. return ui.channels().reduce((acc, c) => acc + c.userCount(), ui.users().length)
  568. }
  569. ui.openContextMenu = (_, event) => openContextMenu(event, this.channelContextMenu, ui)
  570. ui.canJoin = () => {
  571. return true // TODO check for perms
  572. }
  573. ui.canAdd = () => {
  574. return false // TODO check for perms and implement
  575. }
  576. ui.canEdit = () => {
  577. return false // TODO check for perms and implement
  578. }
  579. ui.canRemove = () => {
  580. return false // TODO check for perms and implement
  581. }
  582. ui.canLink = () => {
  583. return false // TODO check for perms and implement
  584. }
  585. ui.canUnlink = () => {
  586. return false // TODO check for perms and implement
  587. }
  588. ui.canSendMessage = () => {
  589. return false // TODO check for perms and implement
  590. }
  591. Object.entries(simpleProperties).forEach(key => {
  592. ui[key[1]] = ko.observable(channel[key[0]])
  593. })
  594. if (channel.parent) {
  595. ui.parent(channel.parent.__ui)
  596. ui.parent().channels.push(ui)
  597. ui.parent().channels.sort(compareChannels)
  598. }
  599. this._updateLinks()
  600. channel.on('update', properties => {
  601. Object.entries(simpleProperties).forEach(key => {
  602. if (properties[key[0]] !== undefined) {
  603. ui[key[1]](properties[key[0]])
  604. }
  605. })
  606. if (properties.parent !== undefined) {
  607. if (ui.parent()) {
  608. ui.parent().channel.remove(ui)
  609. }
  610. ui.parent(properties.parent.__ui)
  611. ui.parent().channels.push(ui)
  612. ui.parent().channels.sort(compareChannels)
  613. }
  614. if (properties.links !== undefined) {
  615. this._updateLinks()
  616. }
  617. }).on('remove', () => {
  618. if (ui.parent()) {
  619. ui.parent().channels.remove(ui)
  620. }
  621. this._updateLinks()
  622. })
  623. }
  624. this.resetClient = () => {
  625. if (this.client) {
  626. this.client.disconnect()
  627. }
  628. this.client = null
  629. this.selected(null).root(null).thisUser(null)
  630. }
  631. this.connected = () => this.thisUser() != null
  632. this._updateVoiceHandler = () => {
  633. if (!this.client) {
  634. return
  635. }
  636. if (voiceHandler) {
  637. voiceHandler.end()
  638. voiceHandler = null
  639. }
  640. let mode = this.settings.voiceMode
  641. if (mode === 'cont') {
  642. voiceHandler = new ContinuousVoiceHandler(this.client, this.settings)
  643. } else if (mode === 'ptt') {
  644. voiceHandler = new PushToTalkVoiceHandler(this.client, this.settings)
  645. } else if (mode === 'vad') {
  646. voiceHandler = new VADVoiceHandler(this.client, this.settings)
  647. } else {
  648. log('Unknown voice mode:', mode)
  649. return
  650. }
  651. voiceHandler.on('started_talking', () => {
  652. if (this.thisUser()) {
  653. this.thisUser().talking('on')
  654. }
  655. })
  656. voiceHandler.on('stopped_talking', () => {
  657. if (this.thisUser()) {
  658. this.thisUser().talking('off')
  659. }
  660. })
  661. if (this.selfMute()) {
  662. voiceHandler.setMute(true)
  663. }
  664. this.client.setAudioQuality(
  665. this.settings.audioBitrate,
  666. this.settings.samplesPerPacket
  667. )
  668. }
  669. this.messageBoxHint = ko.pureComputed(() => {
  670. if (!this.thisUser()) {
  671. return '' // Not yet connected
  672. }
  673. var target = this.selected()
  674. if (!target) {
  675. target = this.thisUser()
  676. }
  677. if (target === this.thisUser()) {
  678. target = target.channel()
  679. }
  680. if (target.users) { // Channel
  681. return "Type message to channel '" + target.name() + "' here"
  682. } else { // User
  683. return "Type message to user '" + target.name() + "' here"
  684. }
  685. })
  686. this.submitMessageBox = () => {
  687. this.sendMessage(this.selected(), this.messageBox())
  688. this.messageBox('')
  689. }
  690. this.sendMessage = (target, message) => {
  691. if (this.connected()) {
  692. // If no target is selected, choose our own user
  693. if (!target) {
  694. target = this.thisUser()
  695. }
  696. // If target is our own user, send to our channel
  697. if (target === this.thisUser()) {
  698. target = target.channel()
  699. }
  700. // Send message
  701. target.model.sendMessage(message)
  702. if (target.users) { // Channel
  703. this.log.push({
  704. type: 'chat-message-self',
  705. message: sanitize(message),
  706. channel: target
  707. })
  708. } else { // User
  709. this.log.push({
  710. type: 'chat-message-self',
  711. message: sanitize(message),
  712. user: target
  713. })
  714. }
  715. }
  716. }
  717. this.requestMove = (user, channel) => {
  718. if (this.connected()) {
  719. user.model.setChannel(channel.model)
  720. }
  721. }
  722. this.requestMute = user => {
  723. if (user === this.thisUser()) {
  724. this.selfMute(true)
  725. }
  726. if (this.connected()) {
  727. if (user === this.thisUser()) {
  728. this.client.setSelfMute(true)
  729. } else {
  730. user.model.setMute(true)
  731. }
  732. }
  733. }
  734. this.requestDeaf = user => {
  735. if (user === this.thisUser()) {
  736. this.selfMute(true)
  737. this.selfDeaf(true)
  738. }
  739. if (this.connected()) {
  740. if (user === this.thisUser()) {
  741. this.client.setSelfDeaf(true)
  742. } else {
  743. user.model.setDeaf(true)
  744. }
  745. }
  746. }
  747. this.requestUnmute = user => {
  748. if (user === this.thisUser()) {
  749. this.selfMute(false)
  750. this.selfDeaf(false)
  751. }
  752. if (this.connected()) {
  753. if (user === this.thisUser()) {
  754. this.client.setSelfMute(false)
  755. } else {
  756. user.model.setMute(false)
  757. }
  758. }
  759. }
  760. this.requestUndeaf = user => {
  761. if (user === this.thisUser()) {
  762. this.selfDeaf(false)
  763. }
  764. if (this.connected()) {
  765. if (user === this.thisUser()) {
  766. this.client.setSelfDeaf(false)
  767. } else {
  768. user.model.setDeaf(false)
  769. }
  770. }
  771. }
  772. this._updateLinks = () => {
  773. if (!this.thisUser()) {
  774. return
  775. }
  776. var allChannels = getAllChannels(this.root(), [])
  777. var ownChannel = this.thisUser().channel().model
  778. var allLinked = findLinks(ownChannel, [])
  779. allChannels.forEach(channel => {
  780. channel.linked(allLinked.indexOf(channel.model) !== -1)
  781. })
  782. function findLinks (channel, knownLinks) {
  783. knownLinks.push(channel)
  784. channel.links.forEach(next => {
  785. if (next && knownLinks.indexOf(next) === -1) {
  786. findLinks(next, knownLinks)
  787. }
  788. })
  789. allChannels.map(c => c.model).forEach(next => {
  790. if (next && knownLinks.indexOf(next) === -1 && next.links.indexOf(channel) !== -1) {
  791. findLinks(next, knownLinks)
  792. }
  793. })
  794. return knownLinks
  795. }
  796. function getAllChannels (channel, channels) {
  797. channels.push(channel)
  798. channel.channels().forEach(next => getAllChannels(next, channels))
  799. return channels
  800. }
  801. }
  802. this.openSourceCode = () => {
  803. var homepage = require('../package.json').homepage
  804. window.open(homepage, '_blank').focus()
  805. }
  806. this.updateSize = () => {
  807. this.minimalView(window.innerWidth < 320)
  808. if (this.minimalView()) {
  809. this.toolbarHorizontal(window.innerWidth < window.innerHeight)
  810. } else {
  811. this.toolbarHorizontal(!this.settings.toolbarVertical)
  812. }
  813. }
  814. }
  815. }
  816. var ui = new GlobalBindings(window.mumbleWebConfig)
  817. // Used only for debugging
  818. window.mumbleUi = ui
  819. window.onload = function () {
  820. var queryParams = url.parse(document.location.href, true).query
  821. queryParams = Object.assign({}, window.mumbleWebConfig.defaults, queryParams)
  822. var useJoinDialog = queryParams.joinDialog
  823. if (queryParams.matrix) {
  824. useJoinDialog = true
  825. }
  826. if (queryParams.address) {
  827. ui.connectDialog.address(queryParams.address)
  828. } else {
  829. useJoinDialog = false
  830. }
  831. if (queryParams.port) {
  832. ui.connectDialog.port(queryParams.port)
  833. } else {
  834. useJoinDialog = false
  835. }
  836. if (queryParams.token) {
  837. var tokens = queryParams.token
  838. if (!Array.isArray(tokens)) {
  839. tokens = [tokens]
  840. }
  841. ui.connectDialog.tokens(tokens)
  842. }
  843. if (queryParams.username) {
  844. ui.connectDialog.username(queryParams.username)
  845. } else {
  846. useJoinDialog = false
  847. }
  848. if (queryParams.password) {
  849. ui.connectDialog.password(queryParams.password)
  850. }
  851. if (queryParams.channelName) {
  852. ui.connectDialog.channelName(queryParams.channelName)
  853. }
  854. if (queryParams.avatarurl) {
  855. // Download the avatar and upload it to the mumble server when connected
  856. let url = queryParams.avatarurl
  857. console.log('Fetching avatar from', url)
  858. let req = new window.XMLHttpRequest()
  859. req.open('GET', url, true)
  860. req.responseType = 'arraybuffer'
  861. req.onload = () => {
  862. let upload = (avatar) => {
  863. if (req.response) {
  864. console.log('Uploading user avatar to server')
  865. ui.client.setSelfTexture(req.response)
  866. }
  867. }
  868. // On any future connections
  869. ui.thisUser.subscribe((thisUser) => {
  870. if (thisUser) {
  871. upload()
  872. }
  873. })
  874. // And the current one (if already connected)
  875. if (ui.thisUser()) {
  876. upload()
  877. }
  878. }
  879. req.send()
  880. }
  881. ui.connectDialog.joinOnly(useJoinDialog)
  882. ko.applyBindings(ui)
  883. }
  884. window.onresize = () => ui.updateSize()
  885. ui.updateSize()
  886. function log () {
  887. console.log.apply(console, arguments)
  888. var args = []
  889. for (var i = 0; i < arguments.length; i++) {
  890. args.push(arguments[i])
  891. }
  892. ui.log.push({
  893. type: 'generic',
  894. value: args.join(' ')
  895. })
  896. }
  897. function compareChannels (c1, c2) {
  898. if (c1.position() === c2.position()) {
  899. return c1.name() === c2.name() ? 0 : c1.name() < c2.name() ? -1 : 1
  900. }
  901. return c1.position() - c2.position()
  902. }
  903. function compareUsers (u1, u2) {
  904. return u1.name() === u2.name() ? 0 : u1.name() < u2.name() ? -1 : 1
  905. }
  906. function userToState () {
  907. var flags = []
  908. // TODO: Friend
  909. if (this.uid()) {
  910. flags.push('Authenticated')
  911. }
  912. // TODO: Priority Speaker, Recording
  913. if (this.mute()) {
  914. flags.push('Muted (server)')
  915. }
  916. if (this.deaf()) {
  917. flags.push('Deafened (server)')
  918. }
  919. // TODO: Local Ignore (Text messages), Local Mute
  920. if (this.selfMute()) {
  921. flags.push('Muted (self)')
  922. }
  923. if (this.selfDeaf()) {
  924. flags.push('Deafened (self)')
  925. }
  926. return flags.join(', ')
  927. }
  928. var voiceHandler
  929. var testVoiceHandler
  930. initVoice(data => {
  931. if (testVoiceHandler) {
  932. testVoiceHandler.write(data)
  933. }
  934. if (!ui.client) {
  935. if (voiceHandler) {
  936. voiceHandler.end()
  937. }
  938. voiceHandler = null
  939. } else if (voiceHandler) {
  940. voiceHandler.write(data)
  941. }
  942. }, err => {
  943. log('Cannot initialize user media. Microphone will not work:', err)
  944. })