12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325 |
- import 'stream-browserify' // see https://github.com/ericgundrum/pouch-websocket-sync-example/commit/2a4437b013092cc7b2cd84cf1499172c84a963a3
- import 'subworkers' // polyfill for https://bugs.chromium.org/p/chromium/issues/detail?id=31666
- import url from 'url'
- import ByteBuffer from 'bytebuffer'
- import MumbleClient from 'mumble-client'
- import WorkerBasedMumbleConnector from './worker-client'
- import BufferQueueNode from 'web-audio-buffer-queue'
- import mumbleConnect from 'mumble-client-websocket'
- 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', '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)
- contextMenu.target(target)
- const closeListener = (event) => {
- // Always close, no matter where they clicked
- setTimeout(() => { // delay to allow click to be actually processed
- contextMenu.target(null)
- unregister()
- })
- }
- const unregister = () => document.removeEventListener('click', closeListener)
- document.addEventListener('click', closeListener)
- event.stopPropagation()
- event.preventDefault()
- }
- // GUI
- function ContextMenu () {
- var self = this
- self.posX = ko.observable()
- self.posY = ko.observable()
- 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('')
- self.port = ko.observable('')
- self.tokenToAdd = ko.observable('')
- self.selectedTokens = ko.observableArray([])
- self.tokens = ko.observableArray([])
- self.username = ko.observable('')
- self.password = ko.observable('')
- self.channelName = ko.observable('')
- self.joinOnly = ko.observable(false)
- self.visible = ko.observable(true)
- self.show = self.visible.bind(self.visible, true)
- self.hide = self.visible.bind(self.visible, false)
- self.connect = function () {
- self.hide()
- if (ui.detectWebRTC) {
- ui.webrtc = true
- }
- ui.connect(self.username(), self.address(), self.port(), self.tokens(), self.password(), self.channelName())
- }
- self.addToken = function() {
- if ((self.tokenToAdd() != "") && (self.tokens.indexOf(self.tokenToAdd()) < 0)) {
- self.tokens.push(self.tokenToAdd())
- }
- self.tokenToAdd("")
- }
- self.removeSelectedTokens = function() {
- this.tokens.removeAll(this.selectedTokens())
- this.selectedTokens([])
- }
- }
- function ConnectErrorDialog (connectDialog) {
- var self = this
- self.type = ko.observable(0)
- self.reason = ko.observable('')
- self.username = connectDialog.username
- self.password = connectDialog.password
- self.joinOnly = connectDialog.joinOnly
- self.visible = ko.observable(false)
- self.show = self.visible.bind(self.visible, true)
- self.hide = self.visible.bind(self.visible, false)
- self.connect = () => {
- self.hide()
- connectDialog.connect()
- }
- }
- class ConnectionInfo {
- constructor (ui) {
- this._ui = ui
- this.visible = ko.observable(false)
- this.serverVersion = ko.observable()
- this.latencyMs = ko.observable(NaN)
- this.latencyDeviation = ko.observable(NaN)
- this.remoteHost = ko.observable()
- this.remotePort = ko.observable()
- this.maxBitrate = ko.observable(NaN)
- this.currentBitrate = ko.observable(NaN)
- this.maxBandwidth = ko.observable(NaN)
- this.currentBandwidth = ko.observable(NaN)
- this.codec = ko.observable()
- this.show = () => {
- if (!ui.thisUser()) return
- this.update()
- this.visible(true)
- }
- this.hide = () => this.visible(false)
- }
- update () {
- let client = this._ui.client
- this.serverVersion(client.serverVersion)
- let dataStats = client.dataStats
- if (dataStats) {
- this.latencyMs(dataStats.mean)
- this.latencyDeviation(Math.sqrt(dataStats.variance))
- }
- this.remoteHost(this._ui.remoteHost())
- this.remotePort(this._ui.remotePort())
- let spp = this._ui.settings.samplesPerPacket
- let maxBitrate = client.getMaxBitrate(spp, false)
- let maxBandwidth = client.maxBandwidth
- let actualBitrate = client.getActualBitrate(spp, false)
- let actualBandwidth = MumbleClient.calcEnforcableBandwidth(actualBitrate, spp, false)
- this.maxBitrate(maxBitrate)
- this.currentBitrate(actualBitrate)
- this.maxBandwidth(maxBandwidth)
- this.currentBandwidth(actualBandwidth)
- this.codec('Opus') // only one supported for sending
- }
- }
- function CommentDialog () {
- var self = this
- self.visible = ko.observable(false)
- self.show = function () {
- self.visible(true)
- }
- }
- class SettingsDialog {
- constructor (settings) {
- this.voiceMode = ko.observable(settings.voiceMode)
- this.pttKey = ko.observable(settings.pttKey)
- this.pttKeyDisplay = ko.observable(settings.pttKey)
- this.vadLevel = ko.observable(settings.vadLevel)
- this.testVadLevel = ko.observable(0)
- this.testVadActive = ko.observable(false)
- this.showAvatars = ko.observable(settings.showAvatars())
- this.userCountInChannelName = ko.observable(settings.userCountInChannelName())
- // Need to wrap this in a pureComputed to make sure it's always numeric
- let audioBitrate = ko.observable(settings.audioBitrate)
- this.audioBitrate = ko.pureComputed({
- read: audioBitrate,
- write: (value) => audioBitrate(Number(value))
- })
- this.samplesPerPacket = ko.observable(settings.samplesPerPacket)
- this.msPerPacket = ko.pureComputed({
- read: () => this.samplesPerPacket() / 48,
- write: (value) => this.samplesPerPacket(value * 48)
- })
- this._setupTestVad()
- this.vadLevel.subscribe(() => this._setupTestVad())
- }
- _setupTestVad () {
- if (this._testVad) {
- this._testVad.end()
- }
- let dummySettings = new Settings({})
- this.applyTo(dummySettings)
- this._testVad = new VADVoiceHandler(null, dummySettings)
- this._testVad.on('started_talking', () => this.testVadActive(true))
- .on('stopped_talking', () => this.testVadActive(false))
- .on('level', level => this.testVadLevel(level))
- testVoiceHandler = this._testVad
- }
- applyTo (settings) {
- settings.voiceMode = this.voiceMode()
- settings.pttKey = this.pttKey()
- settings.vadLevel = this.vadLevel()
- settings.showAvatars(this.showAvatars())
- settings.userCountInChannelName(this.userCountInChannelName())
- settings.audioBitrate = this.audioBitrate()
- settings.samplesPerPacket = this.samplesPerPacket()
- }
- end () {
- this._testVad.end()
- testVoiceHandler = null
- }
- recordPttKey () {
- var combo = []
- const keydown = e => {
- combo = e.pressedKeys
- let comboStr = combo.join(' + ')
- this.pttKeyDisplay('> ' + comboStr + ' <')
- }
- const keyup = () => {
- keyboardjs.unbind('', keydown, keyup)
- let comboStr = combo.join(' + ')
- if (comboStr) {
- this.pttKey(comboStr).pttKeyDisplay(comboStr)
- } else {
- this.pttKeyDisplay(this.pttKey())
- }
- }
- keyboardjs.bind('', keydown, keyup)
- this.pttKeyDisplay('> ? <')
- }
- totalBandwidth () {
- return MumbleClient.calcEnforcableBandwidth(
- this.audioBitrate(),
- this.samplesPerPacket(),
- true
- )
- }
- positionBandwidth () {
- return this.totalBandwidth() - MumbleClient.calcEnforcableBandwidth(
- this.audioBitrate(),
- this.samplesPerPacket(),
- false
- )
- }
- overheadBandwidth () {
- return MumbleClient.calcEnforcableBandwidth(
- 0,
- this.samplesPerPacket(),
- false
- )
- }
- }
- class Settings {
- constructor (defaults) {
- const load = key => window.localStorage.getItem('mumble.' + key)
- this.voiceMode = load('voiceMode') || defaults.voiceMode
- this.pttKey = load('pttKey') || defaults.pttKey
- this.vadLevel = load('vadLevel') || defaults.vadLevel
- this.toolbarVertical = load('toolbarVertical') || defaults.toolbarVertical
- this.showAvatars = ko.observable(load('showAvatars') || defaults.showAvatars)
- this.userCountInChannelName = ko.observable(load('userCountInChannelName') || defaults.userCountInChannelName)
- this.audioBitrate = Number(load('audioBitrate')) || defaults.audioBitrate
- this.samplesPerPacket = Number(load('samplesPerPacket')) || defaults.samplesPerPacket
- }
- save () {
- const save = (key, val) => window.localStorage.setItem('mumble.' + key, val)
- save('voiceMode', this.voiceMode)
- save('pttKey', this.pttKey)
- save('vadLevel', this.vadLevel)
- save('toolbarVertical', this.toolbarVertical)
- save('showAvatars', this.showAvatars())
- save('userCountInChannelName', this.userCountInChannelName())
- save('audioBitrate', this.audioBitrate)
- save('samplesPerPacket', this.samplesPerPacket)
- }
- }
- class GlobalBindings {
- constructor (config) {
- this.config = config
- this.settings = new Settings(config.settings)
- this.detectWebRTC = true
- this.webrtc = true
- this.fallbackConnector = new WorkerBasedMumbleConnector()
- this.webrtcConnector = { connect: mumbleConnect }
- this.client = null
- 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()
- this.settingsDialog = ko.observable()
- this.minimalView = ko.observable(false)
- this.log = ko.observableArray()
- this.remoteHost = ko.observable()
- this.remotePort = ko.observable()
- this.thisUser = ko.observable()
- this.root = ko.observable()
- this.avatarView = ko.observable()
- this.messageBox = ko.observable('')
- this.toolbarHorizontal = ko.observable(!this.settings.toolbarVertical)
- this.selected = ko.observable()
- this.selfMute = ko.observable(this.config.defaults.startMute)
- this.selfDeaf = ko.observable(this.config.defaults.startDeaf)
- this.selfMute.subscribe(mute => {
- if (voiceHandler) {
- voiceHandler.setMute(mute)
- }
- })
- 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()
- this.settings.save()
- }
- this.select = element => {
- this.selected(element)
- }
- this.openSettings = () => {
- 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()
- settingsDialog.applyTo(this.settings)
- this._updateVoiceHandler()
- this.settings.save()
- this.closeSettings()
- }
- this.closeSettings = () => {
- if (this.settingsDialog()) {
- this.settingsDialog().end()
- }
- this.settingsDialog(null)
- }
- this.getTimeString = () => {
- 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)
- this.remotePort(port)
- log(translate('logentry.connecting'), host)
- // Note: This call needs to be delayed until the user has interacted with
- // the page in some way (which at this point they have), see: https://goo.gl/7K7WLu
- let ctx = audioContext()
- this.fallbackConnector.setSampleRate(ctx.sampleRate)
- if (!this._delayedMicNode) {
- this._micNode = ctx.createMediaStreamSource(this._micStream)
- this._delayNode = ctx.createDelay()
- this._delayNode.delayTime.value = 0.15
- this._delayedMicNode = ctx.createMediaStreamDestination()
- }
- // TODO: token
- (this.webrtc ? this.webrtcConnector : this.fallbackConnector).connect(`wss://${host}:${port}`, {
- username: username,
- password: password,
- webrtc: this.webrtc ? {
- enabled: true,
- required: true,
- mic: this._delayedMicNode.stream,
- audioContext: ctx
- } : {
- enabled: false,
- },
- tokens: tokens
- }).done(client => {
- log(translate('logentry.connected'))
- this.client = client
- // Prepare for connection errors
- client.on('error', (err) => {
- log(translate('logentry.connection_error'), err)
- this.resetClient()
- })
- // Make sure we stay open if we're running as Matrix widget
- window.matrixWidget.setAlwaysOnScreen(true)
- // Register all channels, recursively
- if(channelName.indexOf("/") != 0) {
- channelName = "/"+channelName;
- }
- const registerChannel = (channel, channelPath) => {
- this._newChannel(channel)
- if(channelPath === channelName) {
- client.self.setChannel(channel)
- }
- channel.children.forEach(ch => registerChannel(ch, channelPath+"/"+ch.name))
- }
- registerChannel(client.root, "")
- // Register all users
- client.users.forEach(user => this._newUser(user))
- // Register future channels
- client.on('newChannel', channel => this._newChannel(channel))
- // Register future users
- client.on('newUser', user => this._newUser(user))
- // Handle messages
- client.on('message', (sender, message, users, channels, trees) => {
- sender = sender || { __ui: 'Server' }
- ui.log.push({
- type: 'chat-message',
- user: sender.__ui,
- channel: channels.length > 0,
- message: anchorme({input: sanitize(message), options: anchormeOptions})
- })
- })
- // Log permission denied error messages
- client.on('denied', (type) => {
- ui.log.push({
- type: 'generic',
- value: 'Permission denied : '+ type
- })
- })
- // Set own user and root channel
- this.thisUser(client.self.__ui)
- this.root(client.root.__ui)
- // Upate linked channels
- this._updateLinks()
- // Log welcome message
- if (client.welcomeMessage) {
- this.log.push({
- type: 'welcome-message',
- message: sanitize(client.welcomeMessage)
- })
- }
- // Startup audio input processing
- this._updateVoiceHandler()
- // Tell server our mute/deaf state (if necessary)
- if (this.selfDeaf()) {
- this.client.setSelfDeaf(true)
- } else if (this.selfMute()) {
- this.client.setSelfMute(true)
- }
- }, err => {
- if (err.$type && err.$type.name === 'Reject') {
- this.connectErrorDialog.type(err.type)
- this.connectErrorDialog.reason(err.reason)
- this.connectErrorDialog.show()
- } else if (err === 'server_does_not_support_webrtc' && this.detectWebRTC && this.webrtc) {
- log(translate('logentry.connection_fallback_mode'))
- this.webrtc = false
- this.connect(username, host, port, tokens, password, channelName)
- } else {
- log(translate('logentry.connection_error'), err)
- }
- })
- }
- this._newUser = user => {
- const simpleProperties = {
- uniqueId: 'uid',
- username: 'name',
- mute: 'mute',
- deaf: 'deaf',
- suppress: 'suppress',
- selfMute: 'selfMute',
- selfDeaf: 'selfDeaf',
- texture: 'rawTexture',
- textureHash: 'textureHash',
- comment: 'comment'
- }
- var ui = user.__ui = {
- model: user,
- talking: ko.observable('off'),
- channel: ko.observable()
- }
- ui.texture = ko.pureComputed(() => {
- let raw = ui.rawTexture()
- if (!raw || raw.offset >= raw.limit) return null
- return 'data:image/*;base64,' + ByteBuffer.wrap(raw).toBase64()
- })
- ui.show_avatar = () => {
- let setting = this.settings.showAvatars()
- switch (setting) {
- case 'always':
- break
- case 'own_channel':
- if (this.thisUser().channel() !== ui.channel()) return false
- break
- case 'linked_channel':
- if (!ui.channel().linked()) return false
- break
- case 'minimal_only':
- if (!this.minimalView()) return false
- if (this.thisUser().channel() !== ui.channel()) return false
- break
- case 'never':
- default: return false
- }
- if (!ui.texture()) {
- if (ui.textureHash()) {
- // The user has an avatar set but it's of sufficient size to not be
- // included by default, so we need to fetch it explicitly now.
- // mumble-client should make sure we only send one request per hash
- user.requestTexture()
- }
- return false
- }
- return true
- }
- ui.openContextMenu = (_, event) => openContextMenu(event, this.userContextMenu, ui)
- ui.canChangeMute = () => {
- return false // TODO check for perms and implement
- }
- ui.canChangeDeafen = () => {
- return false // TODO check for perms and implement
- }
- ui.canChangePrioritySpeaker = () => {
- return false // TODO check for perms and implement
- }
- ui.canLocalMute = () => {
- return false // TODO implement local mute
- // return this.thisUser() !== ui
- }
- ui.canIgnoreMessages = () => {
- return false // TODO implement ignore messages
- // return this.thisUser() !== ui
- }
- ui.canChangeComment = () => {
- return false // TODO implement changing of comments
- // return this.thisUser() === ui // TODO check for perms
- }
- ui.canChangeAvatar = () => {
- return this.thisUser() === ui // TODO check for perms
- }
- ui.toggleMute = () => {
- if (ui.selfMute()) {
- this.requestUnmute(ui)
- } else {
- this.requestMute(ui)
- }
- }
- ui.toggleDeaf = () => {
- if (ui.selfDeaf()) {
- this.requestUndeaf(ui)
- } else {
- this.requestDeaf(ui)
- }
- }
- ui.viewAvatar = () => {
- this.avatarView(ui.texture())
- }
- ui.changeAvatar = () => {
- let input = document.createElement('input')
- input.type = 'file'
- input.addEventListener('change', () => {
- let reader = new window.FileReader()
- reader.onload = () => {
- this.client.setSelfTexture(reader.result)
- }
- reader.readAsArrayBuffer(input.files[0])
- })
- input.click()
- }
- ui.removeAvatar = () => {
- user.clearTexture()
- }
- Object.entries(simpleProperties).forEach(key => {
- ui[key[1]] = ko.observable(user[key[0]])
- })
- ui.state = ko.pureComputed(userToState, ui)
- if (user.channel) {
- ui.channel(user.channel.__ui)
- ui.channel().users.push(ui)
- ui.channel().users.sort(compareUsers)
- }
- user.on('update', (actor, properties) => {
- Object.entries(simpleProperties).forEach(key => {
- if (properties[key[0]] !== undefined) {
- ui[key[1]](properties[key[0]])
- }
- })
- if (properties.channel !== undefined) {
- if (ui.channel()) {
- ui.channel().users.remove(ui)
- }
- ui.channel(properties.channel.__ui)
- ui.channel().users.push(ui)
- ui.channel().users.sort(compareUsers)
- this._updateLinks()
- }
- if (properties.textureHash !== undefined) {
- // Invalidate avatar texture when its hash has changed
- // If the avatar is still visible, this will trigger a fetch of the new one.
- ui.rawTexture(null)
- }
- }).on('remove', () => {
- if (ui.channel()) {
- ui.channel().users.remove(ui)
- }
- }).on('voice', stream => {
- console.log(`User ${user.username} started takling`)
- let userNode
- if (!this.webrtc) {
- userNode = new BufferQueueNode({
- audioContext: audioContext()
- })
- userNode.connect(audioContext().destination)
- }
- if (stream.target === 'normal') {
- ui.talking('on')
- } else if (stream.target === 'shout') {
- ui.talking('shout')
- } else if (stream.target === 'whisper') {
- ui.talking('whisper')
- }
- stream.on('data', data => {
- if (this.webrtc) {
- // mumble-client is in WebRTC mode, no pcm data should arrive this way
- } else {
- userNode.write(data.buffer)
- }
- }).on('end', () => {
- console.log(`User ${user.username} stopped takling`)
- ui.talking('off')
- if (!this.webrtc) {
- userNode.end()
- }
- })
- })
- }
- this._newChannel = channel => {
- const simpleProperties = {
- position: 'position',
- name: 'name',
- description: 'description'
- }
- var ui = channel.__ui = {
- model: channel,
- expanded: ko.observable(true),
- parent: ko.observable(),
- channels: ko.observableArray(),
- users: ko.observableArray(),
- linked: ko.observable(false)
- }
- ui.userCount = () => {
- return ui.channels().reduce((acc, c) => acc + c.userCount(), ui.users().length)
- }
- ui.openContextMenu = (_, event) => openContextMenu(event, this.channelContextMenu, ui)
- ui.canJoin = () => {
- return true // TODO check for perms
- }
- ui.canAdd = () => {
- return true // TODO check for perms
- }
- ui.canEdit = () => {
- return false // TODO check for perms and implement
- }
- ui.canRemove = () => {
- return true // TODO check for perms
- }
- ui.canLink = () => {
- return false // TODO check for perms and implement
- }
- ui.canUnlink = () => {
- return false // TODO check for perms and implement
- }
- ui.canSendMessage = () => {
- return false // TODO check for perms and implement
- }
- Object.entries(simpleProperties).forEach(key => {
- ui[key[1]] = ko.observable(channel[key[0]])
- })
- if (channel.parent) {
- ui.parent(channel.parent.__ui)
- ui.parent().channels.push(ui)
- ui.parent().channels.sort(compareChannels)
- }
- this._updateLinks()
- channel.on('update', properties => {
- Object.entries(simpleProperties).forEach(key => {
- if (properties[key[0]] !== undefined) {
- ui[key[1]](properties[key[0]])
- }
- })
- if (properties.parent !== undefined) {
- if (ui.parent()) {
- ui.parent().channel.remove(ui)
- }
- ui.parent(properties.parent.__ui)
- ui.parent().channels.push(ui)
- ui.parent().channels.sort(compareChannels)
- }
- if (properties.links !== undefined) {
- this._updateLinks()
- }
- }).on('remove', () => {
- if (ui.parent()) {
- ui.parent().channels.remove(ui)
- }
- this._updateLinks()
- })
- }
- this.resetClient = () => {
- if (this.client) {
- this.client.disconnect()
- }
- this.client = null
- this.selected(null).root(null).thisUser(null)
- }
- this.connected = () => this.thisUser() != null
- this._updateVoiceHandler = () => {
- if (!this.client) {
- return
- }
- if (voiceHandler) {
- voiceHandler.end()
- voiceHandler = null
- }
- let mode = this.settings.voiceMode
- if (mode === 'cont') {
- voiceHandler = new ContinuousVoiceHandler(this.client, this.settings)
- } else if (mode === 'ptt') {
- voiceHandler = new PushToTalkVoiceHandler(this.client, this.settings)
- } else if (mode === 'vad') {
- voiceHandler = new VADVoiceHandler(this.client, this.settings)
- } else {
- log(translate('logentry.unknown_voice_mode'), mode)
- return
- }
- voiceHandler.on('started_talking', () => {
- if (this.thisUser()) {
- this.thisUser().talking('on')
- }
- })
- voiceHandler.on('stopped_talking', () => {
- if (this.thisUser()) {
- this.thisUser().talking('off')
- }
- })
- if (this.selfMute()) {
- voiceHandler.setMute(true)
- }
- this._micNode.disconnect()
- this._delayNode.disconnect()
- if (mode === 'vad') {
- this._micNode.connect(this._delayNode)
- this._delayNode.connect(this._delayedMicNode)
- } else {
- this._micNode.connect(this._delayedMicNode)
- }
- this.client.setAudioQuality(
- this.settings.audioBitrate,
- this.settings.samplesPerPacket
- )
- }
- this.messageBoxHint = ko.pureComputed(() => {
- if (!this.thisUser()) {
- return '' // Not yet connected
- }
- var target = this.selected()
- if (!target) {
- target = this.thisUser()
- }
- if (target === this.thisUser()) {
- target = target.channel()
- }
- if (target.users) { // Channel
- return translate('chat.channel_message_placeholder')
- .replace('%1', target.name())
- } else { // User
- return translate('chat.user_message_placeholder')
- .replace('%1', target.name())
- }
- })
- this.submitMessageBox = () => {
- this.sendMessage(this.selected(), this.messageBox())
- this.messageBox('')
- }
- this.sendMessage = (target, message) => {
- if (this.connected()) {
- // If no target is selected, choose our own user
- if (!target) {
- target = this.thisUser()
- }
- // If target is our own user, send to our channel
- 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(anchorme(message))
- if (target.users) { // Channel
- this.log.push({
- type: 'chat-message-self',
- message: anchorme({input: sanitize(message), options: anchormeOptions}),
- channel: target
- })
- } else { // User
- this.log.push({
- type: 'chat-message-self',
- message: anchorme({input: sanitize(message), options: anchormeOptions}),
- user: target
- })
- }
- }
- }
- this.requestMove = (user, channel) => {
- if (this.connected()) {
- user.model.setChannel(channel.model)
- let currentUrl = url.parse(document.location.href, true)
- // delete search param so that query one can be taken into account
- delete currentUrl.search
- // get full channel path
- if( channel.parent() ){ // in case this channel is not Root
- let parent = channel.parent()
- currentUrl.query.channelName = channel.name()
- while( parent.parent() ){
- currentUrl.query.channelName = parent.name() + '/' + currentUrl.query.channelName
- parent = parent.parent()
- }
- } else {
- // there is no channelName as we moved to Root
- delete currentUrl.query.channelName
- }
- // reflect this change in URL
- window.history.pushState(null, channel.name(), url.format(currentUrl))
- }
- }
- this.requestMute = user => {
- if (user === this.thisUser()) {
- this.selfMute(true)
- }
- if (this.connected()) {
- if (user === this.thisUser()) {
- this.client.setSelfMute(true)
- } else {
- user.model.setMute(true)
- }
- }
- }
- this.requestDeaf = user => {
- if (user === this.thisUser()) {
- this.selfMute(true)
- this.selfDeaf(true)
- }
- if (this.connected()) {
- if (user === this.thisUser()) {
- this.client.setSelfDeaf(true)
- } else {
- user.model.setDeaf(true)
- }
- }
- }
- this.requestUnmute = user => {
- if (user === this.thisUser()) {
- this.selfMute(false)
- this.selfDeaf(false)
- }
- if (this.connected()) {
- if (user === this.thisUser()) {
- this.client.setSelfMute(false)
- } else {
- user.model.setMute(false)
- }
- }
- }
- this.requestUndeaf = user => {
- if (user === this.thisUser()) {
- this.selfDeaf(false)
- }
- if (this.connected()) {
- if (user === this.thisUser()) {
- this.client.setSelfDeaf(false)
- } else {
- user.model.setDeaf(false)
- }
- }
- }
- this._updateLinks = () => {
- if (!this.thisUser()) {
- return
- }
- var allChannels = getAllChannels(this.root(), [])
- var ownChannel = this.thisUser().channel().model
- var allLinked = findLinks(ownChannel, [])
- allChannels.forEach(channel => {
- channel.linked(allLinked.indexOf(channel.model) !== -1)
- })
- function findLinks (channel, knownLinks) {
- knownLinks.push(channel)
- channel.links.forEach(next => {
- if (next && knownLinks.indexOf(next) === -1) {
- findLinks(next, knownLinks)
- }
- })
- allChannels.map(c => c.model).forEach(next => {
- if (next && knownLinks.indexOf(next) === -1 && next.links.indexOf(channel) !== -1) {
- findLinks(next, knownLinks)
- }
- })
- return knownLinks
- }
- function getAllChannels (channel, channels) {
- channels.push(channel)
- channel.channels().forEach(next => getAllChannels(next, channels))
- return channels
- }
- }
- this.openSourceCode = () => {
- var homepage = require('../package.json').homepage
- window.open(homepage, '_blank').focus()
- }
- this.updateSize = () => {
- this.minimalView(window.innerWidth < 320)
- if (this.minimalView()) {
- this.toolbarHorizontal(window.innerWidth < window.innerHeight)
- } else {
- this.toolbarHorizontal(!this.settings.toolbarVertical)
- }
- }
- }
- }
- var ui = new GlobalBindings(window.mumbleWebConfig)
- // Used only for debugging
- window.mumbleUi = ui
- function initializeUI () {
- var queryParams = url.parse(document.location.href, true).query
- queryParams = Object.assign({}, window.mumbleWebConfig.defaults, queryParams)
- var useJoinDialog = queryParams.joinDialog
- if (queryParams.matrix) {
- useJoinDialog = true
- }
- if (queryParams.address) {
- ui.connectDialog.address(queryParams.address)
- } else {
- useJoinDialog = false
- }
- if (queryParams.port) {
- ui.connectDialog.port(queryParams.port)
- } else {
- useJoinDialog = false
- }
- if (queryParams.token) {
- var tokens = queryParams.token
- if (!Array.isArray(tokens)) {
- tokens = [tokens]
- }
- ui.connectDialog.tokens(tokens)
- }
- if (queryParams.username) {
- ui.connectDialog.username(queryParams.username)
- } else {
- useJoinDialog = false
- }
- if (queryParams.password) {
- ui.connectDialog.password(queryParams.password)
- }
- if (queryParams.webrtc !== 'auto') {
- ui.detectWebRTC = false
- if (queryParams.webrtc == 'false') {
- ui.webrtc = false
- }
- }
- if (queryParams.channelName) {
- ui.connectDialog.channelName(queryParams.channelName)
- }
- if (queryParams.avatarurl) {
- // Download the avatar and upload it to the mumble server when connected
- let url = queryParams.avatarurl
- console.log('Fetching avatar from', url)
- let req = new window.XMLHttpRequest()
- req.open('GET', url, true)
- req.responseType = 'arraybuffer'
- req.onload = () => {
- let upload = (avatar) => {
- if (req.response) {
- console.log('Uploading user avatar to server')
- ui.client.setSelfTexture(req.response)
- }
- }
- // On any future connections
- ui.thisUser.subscribe((thisUser) => {
- if (thisUser) {
- upload()
- }
- })
- // And the current one (if already connected)
- if (ui.thisUser()) {
- upload()
- }
- }
- req.send()
- }
- ui.connectDialog.joinOnly(useJoinDialog)
- ko.applyBindings(ui)
- window.onresize = () => ui.updateSize()
- ui.updateSize()
- }
- function log () {
- console.log.apply(console, arguments)
- var args = []
- for (var i = 0; i < arguments.length; i++) {
- args.push(arguments[i])
- }
- ui.log.push({
- type: 'generic',
- value: args.join(' ')
- })
- }
- function compareChannels (c1, c2) {
- if (c1.position() === c2.position()) {
- return c1.name() === c2.name() ? 0 : c1.name() < c2.name() ? -1 : 1
- }
- return c1.position() - c2.position()
- }
- function compareUsers (u1, u2) {
- return u1.name() === u2.name() ? 0 : u1.name() < u2.name() ? -1 : 1
- }
- function userToState () {
- var flags = []
- // TODO: Friend
- if (this.uid()) {
- flags.push('Authenticated')
- }
- // TODO: Priority Speaker, Recording
- if (this.mute()) {
- flags.push('Muted (server)')
- }
- if (this.deaf()) {
- flags.push('Deafened (server)')
- }
- // TODO: Local Ignore (Text messages), Local Mute
- if (this.selfMute()) {
- flags.push('Muted (self)')
- }
- if (this.selfDeaf()) {
- flags.push('Deafened (self)')
- }
- return flags.join(', ')
- }
- var voiceHandler
- var testVoiceHandler
- /**
- * @author svartoyg
- */
- function translatePiece(selector, kind, parameters, key) {
- let element = document.querySelector(selector);
- if (element !== null) {
- const translation = translate(key);
- switch (kind) {
- default:
- console.warn('unhandled dom translation kind "' + kind + '"');
- break;
- case 'textcontent':
- element.textContent = translation;
- break;
- case 'attribute':
- element.setAttribute(parameters.name || 'value', translation);
- break;
- }
- } else {
- console.warn(`translation selector "${selector}" for "${key}" did not match any element`)
- }
- }
- /**
- * @author svartoyg
- */
- function translateEverything() {
- translatePiece('#connect-dialog_title', 'textcontent', {}, 'connectdialog.title');
- translatePiece('#connect-dialog_input_address', 'textcontent', {}, 'connectdialog.address');
- translatePiece('#connect-dialog_input_port', 'textcontent', {}, 'connectdialog.port');
- translatePiece('#connect-dialog_input_username', 'textcontent', {}, 'connectdialog.username');
- translatePiece('#connect-dialog_input_password', 'textcontent', {}, 'connectdialog.password');
- translatePiece('#connect-dialog_input_tokens', 'textcontent', {}, 'connectdialog.tokens');
- translatePiece('#connect-dialog_controls_remove', 'textcontent', {}, 'connectdialog.remove');
- translatePiece('#connect-dialog_controls_add', 'textcontent', {}, 'connectdialog.add');
- translatePiece('#connect-dialog_controls_cancel', 'attribute', {'name': 'value'}, 'connectdialog.cancel');
- translatePiece('#connect-dialog_controls_connect', 'attribute', {'name': 'value'}, 'connectdialog.connect');
- translatePiece('.connect-dialog.error-dialog .dialog-header', 'textcontent', {}, 'connectdialog.error.title');
- translatePiece('.connect-dialog.error-dialog .reason .refused', 'textcontent', {}, 'connectdialog.error.reason.refused');
- translatePiece('.connect-dialog.error-dialog .reason .version', 'textcontent', {}, 'connectdialog.error.reason.version');
- translatePiece('.connect-dialog.error-dialog .reason .username', 'textcontent', {}, 'connectdialog.error.reason.username');
- translatePiece('.connect-dialog.error-dialog .reason .userpassword', 'textcontent', {}, 'connectdialog.error.reason.userpassword');
- translatePiece('.connect-dialog.error-dialog .reason .serverpassword', 'textcontent', {}, 'connectdialog.error.reason.serverpassword');
- translatePiece('.connect-dialog.error-dialog .reason .username-in-use', 'textcontent', {}, 'connectdialog.error.reason.username_in_use');
- translatePiece('.connect-dialog.error-dialog .reason .full', 'textcontent', {}, 'connectdialog.error.reason.full');
- translatePiece('.connect-dialog.error-dialog .reason .clientcert', 'textcontent', {}, 'connectdialog.error.reason.clientcert');
- translatePiece('.connect-dialog.error-dialog .reason .server', 'textcontent', {}, 'connectdialog.error.reason.server');
- translatePiece('.connect-dialog.error-dialog .alternate-username', 'textcontent', {}, 'connectdialog.username');
- translatePiece('.connect-dialog.error-dialog .alternate-password', 'textcontent', {}, 'connectdialog.password');
- translatePiece('.connect-dialog.error-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'connectdialog.error.retry');
- translatePiece('.connect-dialog.error-dialog .dialog-close', 'attribute', {'name': 'value'}, 'connectdialog.error.cancel');
- translatePiece('.join-dialog .dialog-header', 'textcontent', {}, 'joindialog.title');
- translatePiece('.join-dialog .dialog-submit', 'attribute', {'name': 'value'}, 'joindialog.connect');
- translatePiece('.user-context-menu .mute', 'textcontent', {}, 'usercontextmenu.mute');
- translatePiece('.user-context-menu .deafen', 'textcontent', {}, 'usercontextmenu.deafen');
- translatePiece('.user-context-menu .priority-speaker', 'textcontent', {}, 'usercontextmenu.priority_speaker');
- translatePiece('.user-context-menu .local-mute', 'textcontent', {}, 'usercontextmenu.local_mute');
- translatePiece('.user-context-menu .ignore-messages', 'textcontent', {}, 'usercontextmenu.ignore_messages');
- translatePiece('.user-context-menu .view-comment', 'textcontent', {}, 'usercontextmenu.view_comment');
- translatePiece('.user-context-menu .change-comment', 'textcontent', {}, 'usercontextmenu.change_comment');
- translatePiece('.user-context-menu .reset-comment', 'textcontent', {}, 'usercontextmenu.reset_comment');
- translatePiece('.user-context-menu .view-avatar', 'textcontent', {}, 'usercontextmenu.view_avatar');
- translatePiece('.user-context-menu .change-avatar', 'textcontent', {}, 'usercontextmenu.change_avatar');
- translatePiece('.user-context-menu .reset-avatar', 'textcontent', {}, 'usercontextmenu.reset_avatar');
- translatePiece('.user-context-menu .send-message', 'textcontent', {}, 'usercontextmenu.send_message');
- translatePiece('.user-context-menu .information', 'textcontent', {}, 'usercontextmenu.information');
- translatePiece('.user-context-menu .self-mute', 'textcontent', {}, 'usercontextmenu.self_mute');
- translatePiece('.user-context-menu .self-deafen', 'textcontent', {}, 'usercontextmenu.self_deafen');
- translatePiece('.user-context-menu .add-friend', 'textcontent', {}, 'usercontextmenu.add_friend');
- translatePiece('.user-context-menu .remove-friend', 'textcontent', {}, 'usercontextmenu.remove_friend');
- translatePiece('.channel-context-menu .join', 'textcontent', {}, 'channelcontextmenu.join');
- translatePiece('.channel-context-menu .add', 'textcontent', {}, 'channelcontextmenu.add');
- translatePiece('.channel-context-menu .edit', 'textcontent', {}, 'channelcontextmenu.edit');
- translatePiece('.channel-context-menu .remove', 'textcontent', {}, 'channelcontextmenu.remove');
- translatePiece('.channel-context-menu .link', 'textcontent', {}, 'channelcontextmenu.link');
- translatePiece('.channel-context-menu .unlink', 'textcontent', {}, 'channelcontextmenu.unlink');
- translatePiece('.channel-context-menu .unlink-all', 'textcontent', {}, 'channelcontextmenu.unlink_all');
- 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() {
- await localizationInitialize(navigator.language);
- translateEverything();
- try {
- const userMedia = await initVoice(data => {
- if (testVoiceHandler) {
- testVoiceHandler.write(data)
- }
- if (!ui.client) {
- if (voiceHandler) {
- voiceHandler.end()
- }
- voiceHandler = null
- } else if (voiceHandler) {
- voiceHandler.write(data)
- }
- })
- ui._micStream = userMedia
- } catch (err) {
- window.alert('Failed to initialize user media\nRefresh page to retry.\n' + err)
- return
- }
- initializeUI();
- }
- window.onload = main
|