index.js 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331
  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 mumbleConnect from 'mumble-client-websocket'
  9. import audioContext from 'audio-context'
  10. import ko from 'knockout'
  11. import _dompurify from 'dompurify'
  12. import keyboardjs from 'keyboardjs'
  13. import anchorme from 'anchorme'
  14. import { ContinuousVoiceHandler, PushToTalkVoiceHandler, VADVoiceHandler, initVoice } from './voice'
  15. import {initialize as localizationInitialize, translate} from './loc';
  16. const dompurify = _dompurify(window)
  17. // from: https://gist.github.com/haliphax/5379454
  18. ko.extenders.scrollFollow = function (target, selector) {
  19. target.subscribe(function (chat) {
  20. const el = document.querySelector(selector);
  21. // the scroll bar is all the way down, so we know they want to follow the text
  22. if (el.scrollTop == el.scrollHeight - el.clientHeight) {
  23. // have to push our code outside of this thread since the text hasn't updated yet
  24. setTimeout(function () { el.scrollTop = el.scrollHeight - el.clientHeight; }, 0);
  25. } else {
  26. // send notification
  27. const last = chat[chat.length - 1]
  28. if (Notification.permission == 'granted' && last.type != 'chat-message-self') {
  29. let sender = 'Mumble Server'
  30. if (last.user && last.user.name) sender=last.user.name()
  31. new Notification(sender, {body: dompurify.sanitize(last.message, {ALLOWED_TAGS:[]})})
  32. }
  33. }
  34. });
  35. return target;
  36. };
  37. function sanitize (html) {
  38. return dompurify.sanitize(html, {
  39. ALLOWED_TAGS: ['br', 'b', 'i', 'u', 'a', 'span', 'p', 'img', 'center']
  40. })
  41. }
  42. const anchormeOptions = {
  43. // force target _blank attribute
  44. attributes: {
  45. target: "_blank"
  46. },
  47. // force https protocol except email
  48. protocol: function(s) {
  49. if (anchorme.validate.email(s)) {
  50. return "mailto:";
  51. } else {
  52. return "https://";
  53. }
  54. }
  55. }
  56. function openContextMenu (event, contextMenu, target) {
  57. contextMenu.posX(event.clientX)
  58. contextMenu.posY(event.clientY)
  59. contextMenu.target(target)
  60. const closeListener = (event) => {
  61. // Always close, no matter where they clicked
  62. setTimeout(() => { // delay to allow click to be actually processed
  63. contextMenu.target(null)
  64. unregister()
  65. })
  66. }
  67. const unregister = () => document.removeEventListener('click', closeListener)
  68. document.addEventListener('click', closeListener)
  69. event.stopPropagation()
  70. event.preventDefault()
  71. }
  72. // GUI
  73. function ContextMenu () {
  74. var self = this
  75. self.posX = ko.observable()
  76. self.posY = ko.observable()
  77. self.target = ko.observable()
  78. }
  79. function AddChannelDialog () {
  80. var self = this;
  81. self.channelName = ko.observable('')
  82. self.parentID = 0;
  83. self.visible = ko.observable(false);
  84. self.show = self.visible.bind(self.visible, true)
  85. self.hide = self.visible.bind(self.visible, false)
  86. self.addchannel = function() {
  87. self.hide();
  88. ui.addchannel(self.channelName());
  89. }
  90. }
  91. function ConnectDialog () {
  92. var self = this
  93. self.address = ko.observable('')
  94. self.port = ko.observable('')
  95. self.tokenToAdd = ko.observable('')
  96. self.selectedTokens = ko.observableArray([])
  97. self.tokens = ko.observableArray([])
  98. self.username = ko.observable('')
  99. self.password = ko.observable('')
  100. self.channelName = ko.observable('')
  101. self.joinOnly = ko.observable(false)
  102. self.visible = ko.observable(true)
  103. self.show = self.visible.bind(self.visible, true)
  104. self.hide = self.visible.bind(self.visible, false)
  105. self.helpText = ko.observable('')
  106. self.connect = function () {
  107. self.hide()
  108. if (ui.detectWebRTC) {
  109. ui.webrtc = true
  110. }
  111. ui.connect(self.username(), self.address(), self.port(), self.tokens(), self.password(), self.channelName())
  112. }
  113. self.addToken = function() {
  114. if ((self.tokenToAdd() != "") && (self.tokens.indexOf(self.tokenToAdd()) < 0)) {
  115. self.tokens.push(self.tokenToAdd())
  116. }
  117. self.tokenToAdd("")
  118. }
  119. self.removeSelectedTokens = function() {
  120. this.tokens.removeAll(this.selectedTokens())
  121. this.selectedTokens([])
  122. }
  123. }
  124. function ConnectErrorDialog (connectDialog) {
  125. var self = this
  126. self.type = ko.observable(0)
  127. self.reason = ko.observable('')
  128. self.username = connectDialog.username
  129. self.password = connectDialog.password
  130. self.joinOnly = connectDialog.joinOnly
  131. self.visible = ko.observable(false)
  132. self.show = self.visible.bind(self.visible, true)
  133. self.hide = self.visible.bind(self.visible, false)
  134. self.connect = () => {
  135. self.hide()
  136. connectDialog.connect()
  137. }
  138. }
  139. class ConnectionInfo {
  140. constructor (ui) {
  141. this._ui = ui
  142. this.visible = ko.observable(false)
  143. this.serverVersion = ko.observable()
  144. this.latencyMs = ko.observable(NaN)
  145. this.latencyDeviation = ko.observable(NaN)
  146. this.remoteHost = ko.observable()
  147. this.remotePort = ko.observable()
  148. this.maxBitrate = ko.observable(NaN)
  149. this.currentBitrate = ko.observable(NaN)
  150. this.maxBandwidth = ko.observable(NaN)
  151. this.currentBandwidth = ko.observable(NaN)
  152. this.codec = ko.observable()
  153. this.show = () => {
  154. if (!ui.thisUser()) return
  155. this.update()
  156. this.visible(true)
  157. }
  158. this.hide = () => this.visible(false)
  159. }
  160. update () {
  161. let client = this._ui.client
  162. this.serverVersion(client.serverVersion)
  163. let dataStats = client.dataStats
  164. if (dataStats) {
  165. this.latencyMs(dataStats.mean)
  166. this.latencyDeviation(Math.sqrt(dataStats.variance))
  167. }
  168. this.remoteHost(this._ui.remoteHost())
  169. this.remotePort(this._ui.remotePort())
  170. let spp = this._ui.settings.samplesPerPacket
  171. let maxBitrate = client.getMaxBitrate(spp, false)
  172. let maxBandwidth = client.maxBandwidth
  173. let actualBitrate = client.getActualBitrate(spp, false)
  174. let actualBandwidth = MumbleClient.calcEnforcableBandwidth(actualBitrate, spp, false)
  175. this.maxBitrate(maxBitrate)
  176. this.currentBitrate(actualBitrate)
  177. this.maxBandwidth(maxBandwidth)
  178. this.currentBandwidth(actualBandwidth)
  179. this.codec('Opus') // only one supported for sending
  180. }
  181. }
  182. function CommentDialog () {
  183. var self = this
  184. self.visible = ko.observable(false)
  185. self.show = function () {
  186. self.visible(true)
  187. }
  188. }
  189. class SettingsDialog {
  190. constructor (settings) {
  191. this.voiceMode = ko.observable(settings.voiceMode)
  192. this.pttKey = ko.observable(settings.pttKey)
  193. this.pttKeyDisplay = ko.observable(settings.pttKey)
  194. this.vadLevel = ko.observable(settings.vadLevel)
  195. this.testVadLevel = ko.observable(0)
  196. this.testVadActive = ko.observable(false)
  197. this.showAvatars = ko.observable(settings.showAvatars())
  198. this.userCountInChannelName = ko.observable(settings.userCountInChannelName())
  199. // Need to wrap this in a pureComputed to make sure it's always numeric
  200. let audioBitrate = ko.observable(settings.audioBitrate)
  201. this.audioBitrate = ko.pureComputed({
  202. read: audioBitrate,
  203. write: (value) => audioBitrate(Number(value))
  204. })
  205. this.samplesPerPacket = ko.observable(settings.samplesPerPacket)
  206. this.msPerPacket = ko.pureComputed({
  207. read: () => this.samplesPerPacket() / 48,
  208. write: (value) => this.samplesPerPacket(value * 48)
  209. })
  210. this._setupTestVad()
  211. this.vadLevel.subscribe(() => this._setupTestVad())
  212. }
  213. _setupTestVad () {
  214. if (this._testVad) {
  215. this._testVad.end()
  216. }
  217. let dummySettings = new Settings({})
  218. this.applyTo(dummySettings)
  219. this._testVad = new VADVoiceHandler(null, dummySettings)
  220. this._testVad.on('started_talking', () => this.testVadActive(true))
  221. .on('stopped_talking', () => this.testVadActive(false))
  222. .on('level', level => this.testVadLevel(level))
  223. testVoiceHandler = this._testVad
  224. }
  225. applyTo (settings) {
  226. settings.voiceMode = this.voiceMode()
  227. settings.pttKey = this.pttKey()
  228. settings.vadLevel = this.vadLevel()
  229. settings.showAvatars(this.showAvatars())
  230. settings.userCountInChannelName(this.userCountInChannelName())
  231. settings.audioBitrate = this.audioBitrate()
  232. settings.samplesPerPacket = this.samplesPerPacket()
  233. }
  234. end () {
  235. this._testVad.end()
  236. testVoiceHandler = null
  237. }
  238. recordPttKey () {
  239. var combo = []
  240. const keydown = e => {
  241. combo = e.pressedKeys
  242. let comboStr = combo.join(' + ')
  243. this.pttKeyDisplay('> ' + comboStr + ' <')
  244. }
  245. const keyup = () => {
  246. keyboardjs.unbind('', keydown, keyup)
  247. let comboStr = combo.join(' + ')
  248. if (comboStr) {
  249. this.pttKey(comboStr).pttKeyDisplay(comboStr)
  250. } else {
  251. this.pttKeyDisplay(this.pttKey())
  252. }
  253. }
  254. keyboardjs.bind('', keydown, keyup)
  255. this.pttKeyDisplay('> ? <')
  256. }
  257. totalBandwidth () {
  258. return MumbleClient.calcEnforcableBandwidth(
  259. this.audioBitrate(),
  260. this.samplesPerPacket(),
  261. true
  262. )
  263. }
  264. positionBandwidth () {
  265. return this.totalBandwidth() - MumbleClient.calcEnforcableBandwidth(
  266. this.audioBitrate(),
  267. this.samplesPerPacket(),
  268. false
  269. )
  270. }
  271. overheadBandwidth () {
  272. return MumbleClient.calcEnforcableBandwidth(
  273. 0,
  274. this.samplesPerPacket(),
  275. false
  276. )
  277. }
  278. }
  279. class Settings {
  280. constructor (defaults) {
  281. const load = key => window.localStorage.getItem('mumble.' + key)
  282. this.voiceMode = load('voiceMode') || defaults.voiceMode
  283. this.pttKey = load('pttKey') || defaults.pttKey
  284. this.vadLevel = load('vadLevel') || defaults.vadLevel
  285. this.toolbarVertical = load('toolbarVertical') || defaults.toolbarVertical
  286. this.showAvatars = ko.observable(load('showAvatars') || defaults.showAvatars)
  287. this.userCountInChannelName = ko.observable(load('userCountInChannelName') || defaults.userCountInChannelName)
  288. this.audioBitrate = Number(load('audioBitrate')) || defaults.audioBitrate
  289. this.samplesPerPacket = Number(load('samplesPerPacket')) || defaults.samplesPerPacket
  290. }
  291. save () {
  292. const save = (key, val) => window.localStorage.setItem('mumble.' + key, val)
  293. save('voiceMode', this.voiceMode)
  294. save('pttKey', this.pttKey)
  295. save('vadLevel', this.vadLevel)
  296. save('toolbarVertical', this.toolbarVertical)
  297. save('showAvatars', this.showAvatars())
  298. save('userCountInChannelName', this.userCountInChannelName())
  299. save('audioBitrate', this.audioBitrate)
  300. save('samplesPerPacket', this.samplesPerPacket)
  301. }
  302. }
  303. class GlobalBindings {
  304. constructor (config) {
  305. this.config = config
  306. this.settings = new Settings(config.settings)
  307. this.detectWebRTC = true
  308. this.webrtc = true
  309. this.fallbackConnector = new WorkerBasedMumbleConnector()
  310. this.webrtcConnector = { connect: mumbleConnect }
  311. this.client = null
  312. this.userContextMenu = new ContextMenu()
  313. this.channelContextMenu = new ContextMenu()
  314. this.connectDialog = new ConnectDialog()
  315. this.addChannelDialog = new AddChannelDialog()
  316. this.connectErrorDialog = new ConnectErrorDialog(this.connectDialog)
  317. this.connectionInfo = new ConnectionInfo(this)
  318. this.commentDialog = new CommentDialog()
  319. this.settingsDialog = ko.observable()
  320. this.minimalView = ko.observable(false)
  321. this.log = ko.observableArray()
  322. this.remoteHost = ko.observable()
  323. this.remotePort = ko.observable()
  324. this.thisUser = ko.observable()
  325. this.root = ko.observable()
  326. this.avatarView = ko.observable()
  327. this.messageBox = ko.observable('')
  328. this.toolbarHorizontal = ko.observable(!this.settings.toolbarVertical)
  329. this.selected = ko.observable()
  330. this.selfMute = ko.observable(this.config.defaults.startMute)
  331. this.selfDeaf = ko.observable(this.config.defaults.startDeaf)
  332. this.selfMute.subscribe(mute => {
  333. if (voiceHandler) {
  334. voiceHandler.setMute(mute)
  335. }
  336. })
  337. this.submitOnEnter = function(data, e) {
  338. if (e.which == 13 && !e.shiftKey) {
  339. this.submitMessageBox();
  340. return false;
  341. }
  342. return true;
  343. }
  344. this.toggleToolbarOrientation = () => {
  345. this.toolbarHorizontal(!this.toolbarHorizontal())
  346. this.settings.toolbarVertical = !this.toolbarHorizontal()
  347. this.settings.save()
  348. }
  349. this.select = element => {
  350. this.selected(element)
  351. }
  352. this.openSettings = () => {
  353. this.settingsDialog(new SettingsDialog(this.settings))
  354. }
  355. this.openAddChannel = (user, channel) => {
  356. this.addChannelDialog.parentID = channel.model._id;
  357. this.addChannelDialog.show()
  358. }
  359. this.addchannel = (channelName) => {
  360. var msg = {
  361. name: 'ChannelState',
  362. payload: {
  363. parent: this.addChannelDialog.parentID || 0,
  364. name: channelName
  365. }
  366. }
  367. this.client._send(msg);
  368. }
  369. this.ChannelRemove = (user, channel) => {
  370. var msg = {
  371. name: 'ChannelRemove',
  372. payload: {
  373. channel_id: channel.model._id
  374. }
  375. }
  376. this.client._send(msg);
  377. }
  378. this.applySettings = () => {
  379. const settingsDialog = this.settingsDialog()
  380. settingsDialog.applyTo(this.settings)
  381. this._updateVoiceHandler()
  382. this.settings.save()
  383. this.closeSettings()
  384. }
  385. this.closeSettings = () => {
  386. if (this.settingsDialog()) {
  387. this.settingsDialog().end()
  388. }
  389. this.settingsDialog(null)
  390. }
  391. this.getTimeString = () => {
  392. return '[' + new Date().toLocaleTimeString(navigator.language) + ']'
  393. }
  394. this.connect = (username, host, port, tokens = [], password, channelName = "") => {
  395. // if browser support Notification request permission
  396. if ('Notification' in window) Notification.requestPermission()
  397. this.resetClient()
  398. this.remoteHost(host)
  399. this.remotePort(port)
  400. log(translate('logentry.connecting'), host)
  401. // Note: This call needs to be delayed until the user has interacted with
  402. // the page in some way (which at this point they have), see: https://goo.gl/7K7WLu
  403. let ctx = audioContext()
  404. if (!this.webrtc) {
  405. this.fallbackConnector.setSampleRate(ctx.sampleRate)
  406. }
  407. if (!this._delayedMicNode) {
  408. this._micNode = ctx.createMediaStreamSource(this._micStream)
  409. this._delayNode = ctx.createDelay()
  410. this._delayNode.delayTime.value = 0.15
  411. this._delayedMicNode = ctx.createMediaStreamDestination()
  412. }
  413. // TODO: token
  414. (this.webrtc ? this.webrtcConnector : this.fallbackConnector).connect(`wss://${host}:${port}`, {
  415. username: username,
  416. password: password,
  417. webrtc: this.webrtc ? {
  418. enabled: true,
  419. required: true,
  420. mic: this._delayedMicNode.stream,
  421. audioContext: ctx
  422. } : {
  423. enabled: false,
  424. },
  425. tokens: tokens
  426. }).done(client => {
  427. log(translate('logentry.connected'))
  428. this.client = client
  429. // Prepare for connection errors
  430. client.on('error', (err) => {
  431. log(translate('logentry.connection_error'), err)
  432. this.resetClient()
  433. })
  434. // Make sure we stay open if we're running as Matrix widget
  435. window.matrixWidget.setAlwaysOnScreen(true)
  436. // Register all channels, recursively
  437. if(channelName.indexOf("/") != 0) {
  438. channelName = "/"+channelName;
  439. }
  440. const registerChannel = (channel, channelPath) => {
  441. this._newChannel(channel)
  442. if(channelPath === channelName) {
  443. client.self.setChannel(channel)
  444. }
  445. channel.children.forEach(ch => registerChannel(ch, channelPath+"/"+ch.name))
  446. }
  447. registerChannel(client.root, "")
  448. // Register all users
  449. client.users.forEach(user => this._newUser(user))
  450. // Register future channels
  451. client.on('newChannel', channel => this._newChannel(channel))
  452. // Register future users
  453. client.on('newUser', user => this._newUser(user))
  454. // Handle messages
  455. client.on('message', (sender, message, users, channels, trees) => {
  456. sender = sender || { __ui: 'Server' }
  457. ui.log.push({
  458. type: 'chat-message',
  459. user: sender.__ui,
  460. channel: channels.length > 0,
  461. message: anchorme({input: sanitize(message), options: anchormeOptions})
  462. })
  463. })
  464. // Log permission denied error messages
  465. client.on('denied', (type) => {
  466. ui.log.push({
  467. type: 'generic',
  468. value: 'Permission denied : '+ type
  469. })
  470. })
  471. // Set own user and root channel
  472. this.thisUser(client.self.__ui)
  473. this.root(client.root.__ui)
  474. // Upate linked channels
  475. this._updateLinks()
  476. // Log welcome message
  477. if (client.welcomeMessage) {
  478. this.log.push({
  479. type: 'welcome-message',
  480. message: sanitize(client.welcomeMessage)
  481. })
  482. }
  483. // Startup audio input processing
  484. this._updateVoiceHandler()
  485. // Tell server our mute/deaf state (if necessary)
  486. if (this.selfDeaf()) {
  487. this.client.setSelfDeaf(true)
  488. } else if (this.selfMute()) {
  489. this.client.setSelfMute(true)
  490. }
  491. }, err => {
  492. if (err.$type && err.$type.name === 'Reject') {
  493. this.connectErrorDialog.type(err.type)
  494. this.connectErrorDialog.reason(err.reason)
  495. this.connectErrorDialog.show()
  496. } else if (err === 'server_does_not_support_webrtc' && this.detectWebRTC && this.webrtc) {
  497. log(translate('logentry.connection_fallback_mode'))
  498. this.webrtc = false
  499. this.connect(username, host, port, tokens, password, channelName)
  500. } else {
  501. log(translate('logentry.connection_error'), err)
  502. }
  503. })
  504. }
  505. this._newUser = user => {
  506. const simpleProperties = {
  507. uniqueId: 'uid',
  508. username: 'name',
  509. mute: 'mute',
  510. deaf: 'deaf',
  511. suppress: 'suppress',
  512. selfMute: 'selfMute',
  513. selfDeaf: 'selfDeaf',
  514. texture: 'rawTexture',
  515. textureHash: 'textureHash',
  516. comment: 'comment'
  517. }
  518. var ui = user.__ui = {
  519. model: user,
  520. talking: ko.observable('off'),
  521. channel: ko.observable()
  522. }
  523. ui.texture = ko.pureComputed(() => {
  524. let raw = ui.rawTexture()
  525. if (!raw || raw.offset >= raw.limit) return null
  526. return 'data:image/*;base64,' + ByteBuffer.wrap(raw).toBase64()
  527. })
  528. ui.show_avatar = () => {
  529. let setting = this.settings.showAvatars()
  530. switch (setting) {
  531. case 'always':
  532. break
  533. case 'own_channel':
  534. if (this.thisUser().channel() !== ui.channel()) return false
  535. break
  536. case 'linked_channel':
  537. if (!ui.channel().linked()) return false
  538. break
  539. case 'minimal_only':
  540. if (!this.minimalView()) return false
  541. if (this.thisUser().channel() !== ui.channel()) return false
  542. break
  543. case 'never':
  544. default: return false
  545. }
  546. if (!ui.texture()) {
  547. if (ui.textureHash()) {
  548. // The user has an avatar set but it's of sufficient size to not be
  549. // included by default, so we need to fetch it explicitly now.
  550. // mumble-client should make sure we only send one request per hash
  551. user.requestTexture()
  552. }
  553. return false
  554. }
  555. return true
  556. }
  557. ui.openContextMenu = (_, event) => openContextMenu(event, this.userContextMenu, ui)
  558. ui.canChangeMute = () => {
  559. return false // TODO check for perms and implement
  560. }
  561. ui.canChangeDeafen = () => {
  562. return false // TODO check for perms and implement
  563. }
  564. ui.canChangePrioritySpeaker = () => {
  565. return false // TODO check for perms and implement
  566. }
  567. ui.canLocalMute = () => {
  568. return false // TODO implement local mute
  569. // return this.thisUser() !== ui
  570. }
  571. ui.canIgnoreMessages = () => {
  572. return false // TODO implement ignore messages
  573. // return this.thisUser() !== ui
  574. }
  575. ui.canChangeComment = () => {
  576. return false // TODO implement changing of comments
  577. // return this.thisUser() === ui // TODO check for perms
  578. }
  579. ui.canChangeAvatar = () => {
  580. return this.thisUser() === ui // TODO check for perms
  581. }
  582. ui.toggleMute = () => {
  583. if (ui.selfMute()) {
  584. this.requestUnmute(ui)
  585. } else {
  586. this.requestMute(ui)
  587. }
  588. }
  589. ui.toggleDeaf = () => {
  590. if (ui.selfDeaf()) {
  591. this.requestUndeaf(ui)
  592. } else {
  593. this.requestDeaf(ui)
  594. }
  595. }
  596. ui.viewAvatar = () => {
  597. this.avatarView(ui.texture())
  598. }
  599. ui.changeAvatar = () => {
  600. let input = document.createElement('input')
  601. input.type = 'file'
  602. input.addEventListener('change', () => {
  603. let reader = new window.FileReader()
  604. reader.onload = () => {
  605. this.client.setSelfTexture(reader.result)
  606. }
  607. reader.readAsArrayBuffer(input.files[0])
  608. })
  609. input.click()
  610. }
  611. ui.removeAvatar = () => {
  612. user.clearTexture()
  613. }
  614. Object.entries(simpleProperties).forEach(key => {
  615. ui[key[1]] = ko.observable(user[key[0]])
  616. })
  617. ui.state = ko.pureComputed(userToState, ui)
  618. if (user.channel) {
  619. ui.channel(user.channel.__ui)
  620. ui.channel().users.push(ui)
  621. ui.channel().users.sort(compareUsers)
  622. }
  623. user.on('update', (actor, properties) => {
  624. Object.entries(simpleProperties).forEach(key => {
  625. if (properties[key[0]] !== undefined) {
  626. ui[key[1]](properties[key[0]])
  627. }
  628. })
  629. if (properties.channel !== undefined) {
  630. if (ui.channel()) {
  631. ui.channel().users.remove(ui)
  632. }
  633. ui.channel(properties.channel.__ui)
  634. ui.channel().users.push(ui)
  635. ui.channel().users.sort(compareUsers)
  636. this._updateLinks()
  637. }
  638. if (properties.textureHash !== undefined) {
  639. // Invalidate avatar texture when its hash has changed
  640. // If the avatar is still visible, this will trigger a fetch of the new one.
  641. ui.rawTexture(null)
  642. }
  643. }).on('remove', () => {
  644. if (ui.channel()) {
  645. ui.channel().users.remove(ui)
  646. }
  647. }).on('voice', stream => {
  648. console.log(`User ${user.username} started takling`)
  649. let userNode
  650. if (!this.webrtc) {
  651. userNode = new BufferQueueNode({
  652. audioContext: audioContext()
  653. })
  654. userNode.connect(audioContext().destination)
  655. }
  656. if (stream.target === 'normal') {
  657. ui.talking('on')
  658. } else if (stream.target === 'shout') {
  659. ui.talking('shout')
  660. } else if (stream.target === 'whisper') {
  661. ui.talking('whisper')
  662. }
  663. stream.on('data', data => {
  664. if (this.webrtc) {
  665. // mumble-client is in WebRTC mode, no pcm data should arrive this way
  666. } else {
  667. userNode.write(data.buffer)
  668. }
  669. }).on('end', () => {
  670. console.log(`User ${user.username} stopped takling`)
  671. ui.talking('off')
  672. if (!this.webrtc) {
  673. userNode.end()
  674. }
  675. })
  676. })
  677. }
  678. this._newChannel = channel => {
  679. const simpleProperties = {
  680. position: 'position',
  681. name: 'name',
  682. description: 'description'
  683. }
  684. var ui = channel.__ui = {
  685. model: channel,
  686. expanded: ko.observable(true),
  687. parent: ko.observable(),
  688. channels: ko.observableArray(),
  689. users: ko.observableArray(),
  690. linked: ko.observable(false)
  691. }
  692. ui.userCount = () => {
  693. return ui.channels().reduce((acc, c) => acc + c.userCount(), ui.users().length)
  694. }
  695. ui.openContextMenu = (_, event) => openContextMenu(event, this.channelContextMenu, ui)
  696. ui.canJoin = () => {
  697. return true // TODO check for perms
  698. }
  699. ui.canAdd = () => {
  700. return true // TODO check for perms
  701. }
  702. ui.canEdit = () => {
  703. return false // TODO check for perms and implement
  704. }
  705. ui.canRemove = () => {
  706. return true // TODO check for perms
  707. }
  708. ui.canLink = () => {
  709. return false // TODO check for perms and implement
  710. }
  711. ui.canUnlink = () => {
  712. return false // TODO check for perms and implement
  713. }
  714. ui.canSendMessage = () => {
  715. return false // TODO check for perms and implement
  716. }
  717. Object.entries(simpleProperties).forEach(key => {
  718. ui[key[1]] = ko.observable(channel[key[0]])
  719. })
  720. if (channel.parent) {
  721. ui.parent(channel.parent.__ui)
  722. ui.parent().channels.push(ui)
  723. ui.parent().channels.sort(compareChannels)
  724. }
  725. this._updateLinks()
  726. channel.on('update', properties => {
  727. Object.entries(simpleProperties).forEach(key => {
  728. if (properties[key[0]] !== undefined) {
  729. ui[key[1]](properties[key[0]])
  730. }
  731. })
  732. if (properties.parent !== undefined) {
  733. if (ui.parent()) {
  734. ui.parent().channel.remove(ui)
  735. }
  736. ui.parent(properties.parent.__ui)
  737. ui.parent().channels.push(ui)
  738. ui.parent().channels.sort(compareChannels)
  739. }
  740. if (properties.links !== undefined) {
  741. this._updateLinks()
  742. }
  743. }).on('remove', () => {
  744. if (ui.parent()) {
  745. ui.parent().channels.remove(ui)
  746. }
  747. this._updateLinks()
  748. })
  749. }
  750. this.resetClient = () => {
  751. if (this.client) {
  752. this.client.disconnect()
  753. }
  754. this.client = null
  755. this.selected(null).root(null).thisUser(null)
  756. }
  757. this.connected = () => this.thisUser() != null
  758. this._updateVoiceHandler = () => {
  759. if (!this.client) {
  760. return
  761. }
  762. if (voiceHandler) {
  763. voiceHandler.end()
  764. voiceHandler = null
  765. }
  766. let mode = this.settings.voiceMode
  767. if (mode === 'cont') {
  768. voiceHandler = new ContinuousVoiceHandler(this.client, this.settings)
  769. } else if (mode === 'ptt') {
  770. voiceHandler = new PushToTalkVoiceHandler(this.client, this.settings)
  771. } else if (mode === 'vad') {
  772. voiceHandler = new VADVoiceHandler(this.client, this.settings)
  773. } else {
  774. log(translate('logentry.unknown_voice_mode'), mode)
  775. return
  776. }
  777. voiceHandler.on('started_talking', () => {
  778. if (this.thisUser()) {
  779. this.thisUser().talking('on')
  780. }
  781. })
  782. voiceHandler.on('stopped_talking', () => {
  783. if (this.thisUser()) {
  784. this.thisUser().talking('off')
  785. }
  786. })
  787. if (this.selfMute()) {
  788. voiceHandler.setMute(true)
  789. }
  790. this._micNode.disconnect()
  791. this._delayNode.disconnect()
  792. if (mode === 'vad') {
  793. this._micNode.connect(this._delayNode)
  794. this._delayNode.connect(this._delayedMicNode)
  795. } else {
  796. this._micNode.connect(this._delayedMicNode)
  797. }
  798. this.client.setAudioQuality(
  799. this.settings.audioBitrate,
  800. this.settings.samplesPerPacket
  801. )
  802. }
  803. this.messageBoxHint = ko.pureComputed(() => {
  804. if (!this.thisUser()) {
  805. return '' // Not yet connected
  806. }
  807. var target = this.selected()
  808. if (!target) {
  809. target = this.thisUser()
  810. }
  811. if (target === this.thisUser()) {
  812. target = target.channel()
  813. }
  814. if (target.users) { // Channel
  815. return translate('chat.channel_message_placeholder')
  816. .replace('%1', target.name())
  817. } else { // User
  818. return translate('chat.user_message_placeholder')
  819. .replace('%1', target.name())
  820. }
  821. })
  822. this.submitMessageBox = () => {
  823. this.sendMessage(this.selected(), this.messageBox())
  824. this.messageBox('')
  825. }
  826. this.sendMessage = (target, message) => {
  827. if (this.connected()) {
  828. // If no target is selected, choose our own user
  829. if (!target) {
  830. target = this.thisUser()
  831. }
  832. // If target is our own user, send to our channel
  833. if (target === this.thisUser()) {
  834. target = target.channel()
  835. }
  836. // Avoid blank message
  837. if (sanitize(message).trim().length == 0) return;
  838. // Support multiline
  839. message = message.replace(/\n\n+/g,"\n\n");
  840. message = message.replace(/\n/g,"<br>");
  841. // Send message
  842. target.model.sendMessage(anchorme(message))
  843. if (target.users) { // Channel
  844. this.log.push({
  845. type: 'chat-message-self',
  846. message: anchorme({input: sanitize(message), options: anchormeOptions}),
  847. channel: target
  848. })
  849. } else { // User
  850. this.log.push({
  851. type: 'chat-message-self',
  852. message: anchorme({input: sanitize(message), options: anchormeOptions}),
  853. user: target
  854. })
  855. }
  856. }
  857. }
  858. this.requestMove = (user, channel) => {
  859. if (this.connected()) {
  860. user.model.setChannel(channel.model)
  861. let currentUrl = url.parse(document.location.href, true)
  862. // delete search param so that query one can be taken into account
  863. delete currentUrl.search
  864. // get full channel path
  865. if( channel.parent() ){ // in case this channel is not Root
  866. let parent = channel.parent()
  867. currentUrl.query.channelName = channel.name()
  868. while( parent.parent() ){
  869. currentUrl.query.channelName = parent.name() + '/' + currentUrl.query.channelName
  870. parent = parent.parent()
  871. }
  872. } else {
  873. // there is no channelName as we moved to Root
  874. delete currentUrl.query.channelName
  875. }
  876. // reflect this change in URL
  877. window.history.pushState(null, channel.name(), url.format(currentUrl))
  878. }
  879. }
  880. this.requestMute = user => {
  881. if (user === this.thisUser()) {
  882. this.selfMute(true)
  883. }
  884. if (this.connected()) {
  885. if (user === this.thisUser()) {
  886. this.client.setSelfMute(true)
  887. } else {
  888. user.model.setMute(true)
  889. }
  890. }
  891. }
  892. this.requestDeaf = user => {
  893. if (user === this.thisUser()) {
  894. this.selfMute(true)
  895. this.selfDeaf(true)
  896. }
  897. if (this.connected()) {
  898. if (user === this.thisUser()) {
  899. this.client.setSelfDeaf(true)
  900. } else {
  901. user.model.setDeaf(true)
  902. }
  903. }
  904. }
  905. this.requestUnmute = user => {
  906. if (user === this.thisUser()) {
  907. this.selfMute(false)
  908. this.selfDeaf(false)
  909. }
  910. if (this.connected()) {
  911. if (user === this.thisUser()) {
  912. this.client.setSelfMute(false)
  913. } else {
  914. user.model.setMute(false)
  915. }
  916. }
  917. }
  918. this.requestUndeaf = user => {
  919. if (user === this.thisUser()) {
  920. this.selfDeaf(false)
  921. }
  922. if (this.connected()) {
  923. if (user === this.thisUser()) {
  924. this.client.setSelfDeaf(false)
  925. } else {
  926. user.model.setDeaf(false)
  927. }
  928. }
  929. }
  930. this._updateLinks = () => {
  931. if (!this.thisUser()) {
  932. return
  933. }
  934. var allChannels = getAllChannels(this.root(), [])
  935. var ownChannel = this.thisUser().channel().model
  936. var allLinked = findLinks(ownChannel, [])
  937. allChannels.forEach(channel => {
  938. channel.linked(allLinked.indexOf(channel.model) !== -1)
  939. })
  940. function findLinks (channel, knownLinks) {
  941. knownLinks.push(channel)
  942. channel.links.forEach(next => {
  943. if (next && knownLinks.indexOf(next) === -1) {
  944. findLinks(next, knownLinks)
  945. }
  946. })
  947. allChannels.map(c => c.model).forEach(next => {
  948. if (next && knownLinks.indexOf(next) === -1 && next.links.indexOf(channel) !== -1) {
  949. findLinks(next, knownLinks)
  950. }
  951. })
  952. return knownLinks
  953. }
  954. function getAllChannels (channel, channels) {
  955. channels.push(channel)
  956. channel.channels().forEach(next => getAllChannels(next, channels))
  957. return channels
  958. }
  959. }
  960. this.openSourceCode = () => {
  961. var homepage = require('../package.json').homepage
  962. window.open(homepage, '_blank').focus()
  963. }
  964. this.updateSize = () => {
  965. this.minimalView(window.innerWidth < 320)
  966. if (this.minimalView()) {
  967. this.toolbarHorizontal(window.innerWidth < window.innerHeight)
  968. } else {
  969. this.toolbarHorizontal(!this.settings.toolbarVertical)
  970. }
  971. }
  972. }
  973. }
  974. var ui = new GlobalBindings(window.mumbleWebConfig)
  975. // Used only for debugging
  976. window.mumbleUi = ui
  977. function initializeUI () {
  978. var queryParams = url.parse(document.location.href, true).query
  979. queryParams = Object.assign({}, window.mumbleWebConfig.defaults, queryParams)
  980. var useJoinDialog = queryParams.joinDialog
  981. if (queryParams.matrix) {
  982. useJoinDialog = true
  983. }
  984. if (queryParams.address) {
  985. ui.connectDialog.address(queryParams.address)
  986. } else {
  987. useJoinDialog = false
  988. }
  989. if (queryParams.port) {
  990. ui.connectDialog.port(queryParams.port)
  991. } else {
  992. useJoinDialog = false
  993. }
  994. if (queryParams.token) {
  995. var tokens = queryParams.token
  996. if (!Array.isArray(tokens)) {
  997. tokens = [tokens]
  998. }
  999. ui.connectDialog.tokens(tokens)
  1000. }
  1001. if (queryParams.username) {
  1002. ui.connectDialog.username(queryParams.username)
  1003. } else {
  1004. useJoinDialog = false
  1005. }
  1006. if (queryParams.password) {
  1007. ui.connectDialog.password(queryParams.password)
  1008. }
  1009. if (queryParams.webrtc !== 'auto') {
  1010. ui.detectWebRTC = false
  1011. if (queryParams.webrtc == 'false') {
  1012. ui.webrtc = false
  1013. }
  1014. }
  1015. if (queryParams.channelName) {
  1016. ui.connectDialog.channelName(queryParams.channelName)
  1017. }
  1018. if (queryParams.helpText) {
  1019. ui.connectDialog.helpText(queryParams.helpText)
  1020. }
  1021. if (queryParams.avatarurl) {
  1022. // Download the avatar and upload it to the mumble server when connected
  1023. let url = queryParams.avatarurl
  1024. console.log('Fetching avatar from', url)
  1025. let req = new window.XMLHttpRequest()
  1026. req.open('GET', url, true)
  1027. req.responseType = 'arraybuffer'
  1028. req.onload = () => {
  1029. let upload = (avatar) => {
  1030. if (req.response) {
  1031. console.log('Uploading user avatar to server')
  1032. ui.client.setSelfTexture(req.response)
  1033. }
  1034. }
  1035. // On any future connections
  1036. ui.thisUser.subscribe((thisUser) => {
  1037. if (thisUser) {
  1038. upload()
  1039. }
  1040. })
  1041. // And the current one (if already connected)
  1042. if (ui.thisUser()) {
  1043. upload()
  1044. }
  1045. }
  1046. req.send()
  1047. }
  1048. ui.connectDialog.joinOnly(useJoinDialog)
  1049. ko.applyBindings(ui)
  1050. window.onresize = () => ui.updateSize()
  1051. ui.updateSize()
  1052. }
  1053. function log () {
  1054. console.log.apply(console, arguments)
  1055. var args = []
  1056. for (var i = 0; i < arguments.length; i++) {
  1057. args.push(arguments[i])
  1058. }
  1059. ui.log.push({
  1060. type: 'generic',
  1061. value: args.join(' ')
  1062. })
  1063. }
  1064. function compareChannels (c1, c2) {
  1065. if (c1.position() === c2.position()) {
  1066. return c1.name() === c2.name() ? 0 : c1.name() < c2.name() ? -1 : 1
  1067. }
  1068. return c1.position() - c2.position()
  1069. }
  1070. function compareUsers (u1, u2) {
  1071. return u1.name() === u2.name() ? 0 : u1.name() < u2.name() ? -1 : 1
  1072. }
  1073. function userToState () {
  1074. var flags = []
  1075. // TODO: Friend
  1076. if (this.uid()) {
  1077. flags.push('Authenticated')
  1078. }
  1079. // TODO: Priority Speaker, Recording
  1080. if (this.mute()) {
  1081. flags.push('Muted (server)')
  1082. }
  1083. if (this.deaf()) {
  1084. flags.push('Deafened (server)')
  1085. }
  1086. // TODO: Local Ignore (Text messages), Local Mute
  1087. if (this.selfMute()) {
  1088. flags.push('Muted (self)')
  1089. }
  1090. if (this.selfDeaf()) {
  1091. flags.push('Deafened (self)')
  1092. }
  1093. return flags.join(', ')
  1094. }
  1095. var voiceHandler
  1096. var testVoiceHandler
  1097. /**
  1098. * @author svartoyg
  1099. */
  1100. function translatePiece(selector, kind, parameters, key) {
  1101. let element = document.querySelector(selector);
  1102. if (element !== null) {
  1103. const translation = translate(key);
  1104. switch (kind) {
  1105. default:
  1106. console.warn('unhandled dom translation kind "' + kind + '"');
  1107. break;
  1108. case 'textcontent':
  1109. element.textContent = translation;
  1110. break;
  1111. case 'attribute':
  1112. element.setAttribute(parameters.name || 'value', translation);
  1113. break;
  1114. }
  1115. } else {
  1116. console.warn(`translation selector "${selector}" for "${key}" did not match any element`)
  1117. }
  1118. }
  1119. /**
  1120. * @author svartoyg
  1121. */
  1122. function translateEverything() {
  1123. translatePiece('#connect-dialog_title', 'textcontent', {}, 'connectdialog.title');
  1124. translatePiece('#connect-dialog_input_address', 'textcontent', {}, 'connectdialog.address');
  1125. translatePiece('#connect-dialog_input_port', 'textcontent', {}, 'connectdialog.port');
  1126. translatePiece('#connect-dialog_input_username', 'textcontent', {}, 'connectdialog.username');
  1127. translatePiece('#connect-dialog_input_password', 'textcontent', {}, 'connectdialog.password');
  1128. translatePiece('#connect-dialog_input_tokens', 'textcontent', {}, 'connectdialog.tokens');
  1129. translatePiece('#connect-dialog_controls_remove', 'textcontent', {}, 'connectdialog.remove');
  1130. translatePiece('#connect-dialog_controls_add', 'textcontent', {}, 'connectdialog.add');
  1131. translatePiece('#connect-dialog_controls_cancel', 'attribute', {'name': 'value'}, 'connectdialog.cancel');
  1132. translatePiece('#connect-dialog_controls_connect', 'attribute', {'name': 'value'}, 'connectdialog.connect');
  1133. translatePiece('.connect-dialog.error-dialog .dialog-header', 'textcontent', {}, 'connectdialog.error.title');
  1134. translatePiece('.connect-dialog.error-dialog .reason .refused', 'textcontent', {}, 'connectdialog.error.reason.refused');
  1135. translatePiece('.connect-dialog.error-dialog .reason .version', 'textcontent', {}, 'connectdialog.error.reason.version');
  1136. translatePiece('.connect-dialog.error-dialog .reason .username', 'textcontent', {}, 'connectdialog.error.reason.username');
  1137. translatePiece('.connect-dialog.error-dialog .reason .userpassword', 'textcontent', {}, 'connectdialog.error.reason.userpassword');
  1138. translatePiece('.connect-dialog.error-dialog .reason .serverpassword', 'textcontent', {}, 'connectdialog.error.reason.serverpassword');
  1139. translatePiece('.connect-dialog.error-dialog .reason .username-in-use', 'textcontent', {}, 'connectdialog.error.reason.username_in_use');
  1140. translatePiece('.connect-dialog.error-dialog .reason .full', 'textcontent', {}, 'connectdialog.error.reason.full');
  1141. translatePiece('.connect-dialog.error-dialog .reason .clientcert', 'textcontent', {}, 'connectdialog.error.reason.clientcert');
  1142. translatePiece('.connect-dialog.error-dialog .reason .server', 'textcontent', {}, 'connectdialog.error.reason.server');
  1143. translatePiece('.connect-dialog.error-dialog .alternate-username', 'textcontent', {}, 'connectdialog.username');
  1144. translatePiece('.connect-dialog.error-dialog .alternate-password', 'textcontent', {}, 'connectdialog.password');
  1145. translatePiece('.connect-dialog.error-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'connectdialog.error.retry');
  1146. translatePiece('.connect-dialog.error-dialog .dialog-close', 'attribute', {'name': 'value'}, 'connectdialog.error.cancel');
  1147. translatePiece('.join-dialog .dialog-header', 'textcontent', {}, 'joindialog.title');
  1148. translatePiece('.join-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'joindialog.connect');
  1149. translatePiece('.user-context-menu .mute', 'textcontent', {}, 'usercontextmenu.mute');
  1150. translatePiece('.user-context-menu .deafen', 'textcontent', {}, 'usercontextmenu.deafen');
  1151. translatePiece('.user-context-menu .priority-speaker', 'textcontent', {}, 'usercontextmenu.priority_speaker');
  1152. translatePiece('.user-context-menu .local-mute', 'textcontent', {}, 'usercontextmenu.local_mute');
  1153. translatePiece('.user-context-menu .ignore-messages', 'textcontent', {}, 'usercontextmenu.ignore_messages');
  1154. translatePiece('.user-context-menu .view-comment', 'textcontent', {}, 'usercontextmenu.view_comment');
  1155. translatePiece('.user-context-menu .change-comment', 'textcontent', {}, 'usercontextmenu.change_comment');
  1156. translatePiece('.user-context-menu .reset-comment', 'textcontent', {}, 'usercontextmenu.reset_comment');
  1157. translatePiece('.user-context-menu .view-avatar', 'textcontent', {}, 'usercontextmenu.view_avatar');
  1158. translatePiece('.user-context-menu .change-avatar', 'textcontent', {}, 'usercontextmenu.change_avatar');
  1159. translatePiece('.user-context-menu .reset-avatar', 'textcontent', {}, 'usercontextmenu.reset_avatar');
  1160. translatePiece('.user-context-menu .send-message', 'textcontent', {}, 'usercontextmenu.send_message');
  1161. translatePiece('.user-context-menu .information', 'textcontent', {}, 'usercontextmenu.information');
  1162. translatePiece('.user-context-menu .self-mute', 'textcontent', {}, 'usercontextmenu.self_mute');
  1163. translatePiece('.user-context-menu .self-deafen', 'textcontent', {}, 'usercontextmenu.self_deafen');
  1164. translatePiece('.user-context-menu .add-friend', 'textcontent', {}, 'usercontextmenu.add_friend');
  1165. translatePiece('.user-context-menu .remove-friend', 'textcontent', {}, 'usercontextmenu.remove_friend');
  1166. translatePiece('.channel-context-menu .join', 'textcontent', {}, 'channelcontextmenu.join');
  1167. translatePiece('.channel-context-menu .add', 'textcontent', {}, 'channelcontextmenu.add');
  1168. translatePiece('.channel-context-menu .edit', 'textcontent', {}, 'channelcontextmenu.edit');
  1169. translatePiece('.channel-context-menu .remove', 'textcontent', {}, 'channelcontextmenu.remove');
  1170. translatePiece('.channel-context-menu .link', 'textcontent', {}, 'channelcontextmenu.link');
  1171. translatePiece('.channel-context-menu .unlink', 'textcontent', {}, 'channelcontextmenu.unlink');
  1172. translatePiece('.channel-context-menu .unlink-all', 'textcontent', {}, 'channelcontextmenu.unlink_all');
  1173. translatePiece('.channel-context-menu .copy-mumble-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_url');
  1174. translatePiece('.channel-context-menu .copy-mumble-web-url', 'textcontent', {}, 'channelcontextmenu.copy_mumble_web_url');
  1175. translatePiece('.channel-context-menu .send-message', 'textcontent', {}, 'channelcontextmenu.send_message');
  1176. translatePiece('.toolbar .tb-horizontal', 'attribute', {'name': 'title'}, 'toolbar.orientation');
  1177. translatePiece('.toolbar .tb-horizontal', 'attribute', {'name': 'alt'}, 'toolbar.orientation');
  1178. translatePiece('.toolbar .tb-vertical', 'attribute', {'name': 'title'}, 'toolbar.orientation');
  1179. translatePiece('.toolbar .tb-vertical', 'attribute', {'name': 'alt'}, 'toolbar.orientation');
  1180. translatePiece('.toolbar .tb-connect', 'attribute', {'name': 'title'}, 'toolbar.connect');
  1181. translatePiece('.toolbar .tb-connect', 'attribute', {'name': 'alt'}, 'toolbar.connect');
  1182. translatePiece('.toolbar .tb-information', 'attribute', {'name': 'title'}, 'toolbar.information');
  1183. translatePiece('.toolbar .tb-information', 'attribute', {'name': 'alt'}, 'toolbar.information');
  1184. translatePiece('.toolbar .tb-mute', 'attribute', {'name': 'title'}, 'toolbar.mute');
  1185. translatePiece('.toolbar .tb-mute', 'attribute', {'name': 'alt'}, 'toolbar.mute');
  1186. translatePiece('.toolbar .tb-unmute', 'attribute', {'name': 'title'}, 'toolbar.unmute');
  1187. translatePiece('.toolbar .tb-unmute', 'attribute', {'name': 'alt'}, 'toolbar.unmute');
  1188. translatePiece('.toolbar .tb-deaf', 'attribute', {'name': 'title'}, 'toolbar.deaf');
  1189. translatePiece('.toolbar .tb-deaf', 'attribute', {'name': 'alt'}, 'toolbar.deaf');
  1190. translatePiece('.toolbar .tb-undeaf', 'attribute', {'name': 'title'}, 'toolbar.undeaf');
  1191. translatePiece('.toolbar .tb-undeaf', 'attribute', {'name': 'alt'}, 'toolbar.undeaf');
  1192. translatePiece('.toolbar .tb-record', 'attribute', {'name': 'title'}, 'toolbar.record');
  1193. translatePiece('.toolbar .tb-record', 'attribute', {'name': 'alt'}, 'toolbar.record');
  1194. translatePiece('.toolbar .tb-comment', 'attribute', {'name': 'title'}, 'toolbar.comment');
  1195. translatePiece('.toolbar .tb-comment', 'attribute', {'name': 'alt'}, 'toolbar.comment');
  1196. translatePiece('.toolbar .tb-settings', 'attribute', {'name': 'title'}, 'toolbar.settings');
  1197. translatePiece('.toolbar .tb-settings', 'attribute', {'name': 'alt'}, 'toolbar.settings');
  1198. translatePiece('.toolbar .tb-sourcecode', 'attribute', {'name': 'title'}, 'toolbar.sourcecode');
  1199. translatePiece('.toolbar .tb-sourcecode', 'attribute', {'name': 'alt'}, 'toolbar.sourcecode');
  1200. }
  1201. async function main() {
  1202. await localizationInitialize(navigator.language);
  1203. translateEverything();
  1204. try {
  1205. const userMedia = await initVoice(data => {
  1206. if (testVoiceHandler) {
  1207. testVoiceHandler.write(data)
  1208. }
  1209. if (!ui.client) {
  1210. if (voiceHandler) {
  1211. voiceHandler.end()
  1212. }
  1213. voiceHandler = null
  1214. } else if (voiceHandler) {
  1215. voiceHandler.write(data)
  1216. }
  1217. })
  1218. ui._micStream = userMedia
  1219. } catch (err) {
  1220. window.alert('Failed to initialize user media\nRefresh page to retry.\n' + err)
  1221. return
  1222. }
  1223. initializeUI();
  1224. }
  1225. window.onload = main