diff --git a/app/assets/javascripts/app/controllers/chat.coffee b/app/assets/javascripts/app/controllers/chat.coffee
new file mode 100644
index 000000000..9eb72f8ee
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/chat.coffee
@@ -0,0 +1,383 @@
+class App.CustomerChat extends App.Controller
+ @extend Spine.Events
+
+ events:
+ 'click .js-acceptChat': 'acceptChat'
+
+ sounds:
+ chat_new: new Audio('assets/sounds/chat_new.mp3')
+
+ constructor: ->
+ super
+
+ @i = 0
+ @chatWindows = {}
+ @totalQuestions = 7
+ @answered = 0
+ @correct = 0
+ @wrong = 0
+ @maxChats = 4
+
+ @messageCounter = 0
+ @meta =
+ active: false
+ waiting_chat_count: 0
+ running_chat_count: 0
+ active_agents: 0
+
+ @render()
+
+ App.Event.bind(
+ 'chat_status_agent'
+ (data) =>
+ @meta = data
+ @updateMeta()
+ )
+ App.Event.bind(
+ 'chat_session_start'
+ (data) =>
+ App.WebSocket.send(event:'chat_status_agent')
+ if data.session
+ @addChat(data.session)
+ )
+
+ App.WebSocket.send(event:'chat_status_agent')
+
+ @interval(@pushState, 16000)
+
+ pushState: =>
+ App.WebSocket.send(
+ event:'chat_agent_state'
+ data:
+ active: @meta.active
+ )
+
+ render: ->
+ @html App.view('customer_chat/index')()
+
+ show: (params) =>
+ @navupdate '#customer_chat'
+
+ counter: =>
+ if @meta.waiting_chat_count
+ if @waitingChatCountLast isnt @meta.waiting_chat_count
+ @sounds.chat_new.play()
+ @waitingChatCountLast = @meta.waiting_chat_count
+
+ return @messageCounter + @meta.waiting_chat_count
+ @messageCounter
+
+ switch: (state = undefined) =>
+
+ # read state
+ if state is undefined
+ return @meta.active
+
+ @meta.active = state
+
+ # write state
+ App.WebSocket.send(
+ event:'chat_agent_state'
+ data:
+ active: @meta.active
+ )
+
+ updateNavMenu: =>
+ delay = ->
+ App.Event.trigger('menu:render')
+ @delay(delay, 200)
+
+ updateMeta: =>
+ if @meta.waiting_chat_count
+ @$('.js-acceptChat').addClass('is-clickable is-blinking')
+ else
+ @$('.js-acceptChat').removeClass('is-clickable is-blinking')
+ @$('.js-badgeWaitingCustomers').text(@meta.waiting_chat_count)
+ @$('.js-badgeChattingCustomers').text(@meta.running_chat_count)
+ @$('.js-badgeActiveAgents').text(@meta.active_agents)
+
+ if @meta.active_sessions
+ for session in @meta.active_sessions
+ @addChat(session)
+
+ @updateNavMenu()
+
+ addChat: (session) ->
+ return if @chatWindows[session.session_id]
+ chat = new chatWindow
+ name: "#{session.created_at}"
+ session: session
+ callback: @removeChat
+
+ @on 'layout-has-changed', @propagateLayoutChange
+
+ @$('.chat-workspace').append(chat.el)
+ @chatWindows[session.session_id] = chat
+
+ removeChat: (session_id) =>
+ console.log('removeChat', session_id, @chatWindows[session_id])
+ delete @chatWindows[session_id]
+
+ propagateLayoutChange: (event) =>
+
+ # adjust scroll position on layoutChange
+ for session_id, chat of @chatWindows
+ chat.trigger 'layout-changed'
+
+ acceptChat: =>
+ currentChats = 0
+ for key, value of @chatWindows
+ if @chatWindows[key]
+ currentChats += 1
+ return if currentChats >= @maxChats
+
+ App.WebSocket.send(event:'chat_session_start')
+
+class CustomerChatRouter extends App.ControllerPermanent
+ constructor: (params) ->
+ super
+
+ # check authentication
+ return if !@authenticate()
+
+ App.TaskManager.execute(
+ key: 'CustomerChat'
+ controller: 'CustomerChat'
+ params: {}
+ show: true
+ persistent: true
+ )
+
+class chatWindow extends App.Controller
+ @extend Spine.Events
+
+ className: 'chat-window'
+
+ events:
+ 'keydown .js-customerChatInput': 'onKeydown'
+ 'focus .js-customerChatInput': 'clearUnread'
+ 'click': 'clearUnread'
+ 'click .js-send': 'sendMessage'
+ 'click .js-close': 'close'
+
+ elements:
+ '.js-customerChatInput': 'input'
+ '.js-status': 'status'
+ '.js-body': 'body'
+ '.js-scrollHolder': 'scrollHolder'
+
+ sounds:
+ message: new Audio('assets/sounds/chat_message.mp3')
+
+ constructor: ->
+ super
+
+ @showTimeEveryXMinutes = 1
+ @lastTimestamp
+ @lastAddedType
+ @isTyping = false
+ @render()
+
+ @on 'layout-change', @scrollToBottom
+
+ App.Event.bind(
+ 'chat_session_typing'
+ (data) =>
+ return if data.session_id isnt @session.session_id
+ return if data.self_written
+ @showWritingLoader()
+ )
+ App.Event.bind(
+ 'chat_session_message'
+ (data) =>
+ return if data.session_id isnt @session.session_id
+ return if data.self_written
+ @receiveMessage(data.message.content)
+ )
+
+ render: ->
+ @html App.view('layout_ref/customer_chat_window')
+ name: @options.name
+
+ @el.one 'transitionend', @onTransitionend
+
+ # make sure animation will run
+ setTimeout (=> @el.addClass('is-open')), 0
+
+ # @addMessage 'Hello. My name is Roger, how can I help you?', 'agent'
+ if @session && @session.messages
+ for message in @session.messages
+ if message.created_by_id
+ @addMessage message.content, 'agent'
+ else
+ @addMessage message.content, 'customer'
+
+ # set focus
+ @input.get(0).focus()
+
+ onTransitionend: (event) =>
+ # chat window is done with animation - adjust scroll-bars
+ # of sibling chat windows
+ @trigger 'layout-has-changed'
+
+ if event.data and event.data.callback
+ event.data.callback()
+
+ close: =>
+ @el.one 'transitionend', { callback: @release }, @onTransitionend
+ @el.removeClass('is-open')
+ App.WebSocket.send(
+ event:'chat_session_close'
+ data:
+ session_id: @session.session_id
+ )
+ if @callback
+ @callback(@session.session_id)
+
+ release: =>
+ @trigger 'closed'
+ super
+
+ clearUnread: =>
+ @$('.chat-message--new').removeClass('chat-message--new')
+ @updateModified(false)
+
+ onKeydown: (event) =>
+ TABKEY = 9;
+ ENTERKEY = 13;
+
+ if event.keyCode isnt TABKEY && event.keyCode isnt ENTERKEY
+ App.WebSocket.send(
+ event:'chat_session_typing'
+ data:
+ session_id: @session.session_id
+ )
+
+ switch event.keyCode
+ when TABKEY
+ allChatInputs = $('.js-customerChatInput').not('[disabled="disabled"]')
+ chatCount = allChatInputs.size()
+ index = allChatInputs.index(@input)
+
+ if chatCount > 1
+ switch index
+ when chatCount-1
+ if !event.shiftKey
+ # State: tab without shift on last input
+ # Jump to first input
+ event.preventDefault()
+ allChatInputs.eq(0).focus()
+ when 0
+ if event.shiftKey
+ # State: tab with shift on first input
+ # Jump to last input
+ event.preventDefault()
+ allChatInputs.eq(chatCount-1).focus()
+
+ when ENTERKEY
+ if !event.shiftKey
+ event.preventDefault()
+ @sendMessage()
+
+ sendMessage: =>
+ content = @input.html()
+ return if !content
+
+ #@trigger "answer", @input.html()
+ App.WebSocket.send(
+ event:'chat_session_message'
+ data:
+ content: content
+ session_id: @session.session_id
+ )
+
+ @addMessage content, 'agent'
+ @input.html('')
+
+ updateModified: (state) =>
+ @status.toggleClass('is-modified', state)
+
+ receiveMessage: (message) =>
+ isFocused = @input.is(':focus')
+
+ @removeWritingLoader()
+ @addMessage(message, 'customer', !isFocused)
+
+ if !isFocused
+ @updateModified(true)
+ @sounds.message.play()
+
+ addMessage: (message, sender, isNew) =>
+ @maybeAddTimestamp()
+
+ @lastAddedType = sender
+
+ @body.append App.view('layout_ref/customer_chat_message')
+ message: message
+ sender: sender
+ isNew: isNew
+ timestamp: Date.now()
+
+ @scrollToBottom()
+
+ showWritingLoader: =>
+ if !@isTyping
+ @isTyping = true
+ @maybeAddTimestamp()
+ @body.append App.view('layout_ref/customer_chat_loader')()
+ @scrollToBottom()
+
+ # clear old delay, set new
+ @delay(@removeWritingLoader, 1800, 'typing')
+
+ removeWritingLoader: =>
+ @isTyping = false
+ @$('.js-loader').remove()
+
+ goOffline: =>
+ @addStatusMessage("#{ @options.name }'s connection got closed")
+ @status.attr('data-status', 'offline')
+ @el.addClass('is-offline')
+ @input.attr('disabled', true)
+
+ maybeAddTimestamp: ->
+ timestamp = Date.now()
+
+ if !@lastTimestamp or timestamp - @lastTimestamp > @showTimeEveryXMinutes * 60000
+ label = 'Today'
+ time = new Date().toTimeString().substr(0,5)
+ if @lastAddedType is 'timestamp'
+ # update last time
+ @updateLastTimestamp label, time
+ @lastTimestamp = timestamp
+ else
+ @addTimestamp label, time
+ @lastTimestamp = timestamp
+ @lastAddedType = 'timestamp'
+
+ addTimestamp: (label, time) =>
+ @body.append App.view('layout_ref/customer_chat_timestamp')
+ label: label
+ time: time
+
+ updateLastTimestamp: (label, time) ->
+ @body
+ .find('.js-timestamp')
+ .last()
+ .replaceWith App.view('layout_ref/customer_chat_timestamp')
+ label: label
+ time: time
+
+ addStatusMessage: (message) ->
+ @body.append App.view('layout_ref/customer_chat_status_message')
+ message: message
+
+ @scrollToBottom()
+
+ scrollToBottom: ->
+ @scrollHolder.scrollTop(@scrollHolder.prop('scrollHeight'))
+
+
+App.Config.set( 'customer_chat', CustomerChatRouter, 'Routes' )
+App.Config.set( 'CustomerChat', { controller: 'CustomerChat', authentication: true }, 'permanentTask' )
+App.Config.set( 'CustomerChat', { prio: 1200, parent: '', name: 'Customer Chat', target: '#customer_chat', key: 'CustomerChat', role: ['Chat'], class: 'chat' }, 'NavBar' )
diff --git a/app/assets/javascripts/app/views/customer_chat/index.jst.eco b/app/assets/javascripts/app/views/customer_chat/index.jst.eco
new file mode 100644
index 000000000..c13088449
--- /dev/null
+++ b/app/assets/javascripts/app/views/customer_chat/index.jst.eco
@@ -0,0 +1,24 @@
+
\ No newline at end of file
diff --git a/app/assets/javascripts/app/views/navigation/menu.jst.eco b/app/assets/javascripts/app/views/navigation/menu.jst.eco
index ef2f79320..e78b7f188 100644
--- a/app/assets/javascripts/app/views/navigation/menu.jst.eco
+++ b/app/assets/javascripts/app/views/navigation/menu.jst.eco
@@ -26,7 +26,7 @@
- <% if item.counter isnt undefined: %>
+ <% if item.counter isnt undefined && item.counter isnt 0: %>
<%= item.counter %>
<% end %>
<% if item.switch isnt undefined: %>
diff --git a/app/models/chat.rb b/app/models/chat.rb
new file mode 100644
index 000000000..8ec1536a9
--- /dev/null
+++ b/app/models/chat.rb
@@ -0,0 +1,133 @@
+# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
+
+class Chat < ApplicationModel
+ has_many :chat_topics
+ validates :name, presence: true
+
+ def state
+ return { state: 'chat_disabled' } if !Setting.get('chat')
+
+ if Chat::Agent.where(active: true).where('updated_at > ?', Time.zone.now - 2.minutes).count > 0
+ if active_chat_count >= max_queue
+ return {
+ state: 'no_seats_available',
+ queue: seads_available,
+ }
+ else
+ return { state: 'online' }
+ end
+ end
+
+ { state: 'offline' }
+ end
+
+ def self.agent_state(user_id)
+ return { state: 'chat_disabled' } if !Setting.get('chat')
+ actice_sessions = []
+ Chat::Session.where(state: 'running').order('created_at ASC').each {|session|
+ session_attributes = session.attributes
+ session_attributes['messages'] = []
+ Chat::Message.where(chat_session_id: session.id).each { |message|
+ session_attributes['messages'].push message.attributes
+ }
+ actice_sessions.push session_attributes
+ }
+ {
+ waiting_chat_count: waiting_chat_count,
+ running_chat_count: running_chat_count,
+ #available_agents: available_agents,
+ active_sessions: actice_sessions,
+ active: Chat::Agent.state(user_id)
+ }
+ end
+
+ def self.waiting_chat_count
+ Chat::Session.where(state: ['waiting']).count
+ end
+
+ def self.running_chat_count
+ Chat::Session.where(state: ['waiting']).count
+ end
+
+ def active_chat_count
+ Chat::Session.where(state: %w(waiting running)).count
+ end
+
+ def available_agents(diff = 2.minutes)
+ agents = {}
+ Chat::Agent.where(active: true).where('updated_at > ?', Time.zone.now - diff).each {|record|
+ agents[record.updated_by_id] = record.concurrent
+ }
+ agents
+ end
+
+ def seads_total(diff = 2.minutes)
+ total = 0
+ available_agents(diff).each {|_record, concurrent|
+ total += concurrent
+ }
+ total
+ end
+
+ def seads_available(diff = 2.minutes)
+ seads_total(diff) - active_chat_count
+ end
+end
+
+class Chat::Topic < ApplicationModel
+end
+
+class Chat::Agent < ApplicationModel
+
+ def seads_available
+ concurrent - active_chat_count
+ end
+
+ def active_chat_count
+ Chat::Session.where(state: %w(waiting running), user_id: updated_by_id).count
+ end
+
+ def self.state(user_id, state = nil)
+ chat_agent = Chat::Agent.find_by(
+ updated_by_id: user_id
+ )
+ if state.nil?
+ return false if !chat_agent
+ return chat_agent.active
+ end
+ if chat_agent
+ chat_agent.active = state
+ chat_agent.updated_at = Time.zone.now
+ chat_agent.save
+ else
+ Chat::Agent.create(
+ active: state,
+ updated_by_id: user_id,
+ created_by_id: user_id,
+ )
+ end
+ end
+
+ def self.create_or_update(params)
+ chat_agent = Chat::Agent.find_by(
+ updated_by_id: params[:updated_by_id]
+ )
+ if chat_agent
+ chat_agent.update_attributes(params)
+ else
+ Chat::Agent.create(params)
+ end
+ end
+end
+
+class Chat::Session < ApplicationModel
+ before_create :generate_session_id
+ store :preferences
+
+ def generate_session_id
+ self.session_id = Digest::MD5.hexdigest(Time.zone.now.to_s + rand(99_999_999_999_999).to_s)
+ end
+end
+
+class Chat::Message < ApplicationModel
+end
diff --git a/db/migrate/20151109000001_create_chat.rb b/db/migrate/20151109000001_create_chat.rb
new file mode 100644
index 000000000..85992f14c
--- /dev/null
+++ b/db/migrate/20151109000001_create_chat.rb
@@ -0,0 +1,93 @@
+class CreateChat < ActiveRecord::Migration
+ def up
+=begin
+ ActiveRecord::Migration.drop_table :chats
+ ActiveRecord::Migration.drop_table :chat_topics
+ ActiveRecord::Migration.drop_table :chat_sessions
+ ActiveRecord::Migration.drop_table :chat_messages
+ ActiveRecord::Migration.drop_table :chat_agents
+=end
+ create_table :chats do |t|
+ t.string :name, limit: 250, null: true
+ t.integer :max_queue, null: false, default: 5
+ t.string :note, limit: 250, null: true
+ t.boolean :active, null: false, default: true
+ t.integer :updated_by_id, null: false
+ t.integer :created_by_id, null: false
+ t.timestamps null: false
+ end
+ add_index :chats, [:name], unique: true
+
+ create_table :chat_topics do |t|
+ t.integer :chat_id, null: false
+ t.string :name, limit: 250, null: false
+ t.string :note, limit: 250, null: true
+ t.integer :updated_by_id, null: false
+ t.integer :created_by_id, null: false
+ t.timestamps null: false
+ end
+ add_index :chat_topics, [:name], unique: true
+
+ create_table :chat_sessions do |t|
+ t.integer :chat_id, null: false
+ t.string :session_id, null: false
+ t.string :name, limit: 250, null: true
+ t.string :state, limit: 50, null: false, default: 'waiting' # running, closed
+ t.integer :user_id, null: true
+ t.text :preferences, limit: 100.kilobytes + 1, null: true
+ t.integer :updated_by_id, null: true
+ t.integer :created_by_id, null: true
+ t.timestamps null: false
+ end
+ add_index :chat_sessions, [:state]
+ add_index :chat_sessions, [:user_id]
+
+ create_table :chat_messages do |t|
+ t.integer :chat_session_id, null: false
+ t.string :content, limit: 5000, null: false
+ t.integer :created_by_id, null: true
+ t.timestamps null: false
+ end
+
+ create_table :chat_agents do |t|
+ t.boolean :active, null: false, default: true
+ t.integer :concurrent, null: false, default: 5
+ t.integer :updated_by_id, null: false
+ t.integer :created_by_id, null: false
+ t.timestamps null: false
+ end
+ add_index :chat_agents, [:created_by_id], unique: true
+
+ Role.create_if_not_exists(
+ name: 'Chat',
+ note: 'Access to chat feature.',
+ updated_by_id: 1,
+ created_by_id: 1
+ )
+
+ chat = Chat.create(
+ name: 'default',
+ max_queue: 5,
+ note: '',
+ active: true,
+ updated_by_id: 1,
+ created_by_id: 1,
+ )
+
+ chat_topic = Chat::Topic.create(
+ chat_id: chat.id,
+ name: 'default',
+ updated_by_id: 1,
+ created_by_id: 1,
+ )
+
+ end
+
+ def down
+ drop_table :chat_topics
+ drop_table :chat_sessions
+ drop_table :chat_messages
+ drop_table :chat_agents
+ drop_table :chats
+ end
+end
diff --git a/lib/sessions/event.rb b/lib/sessions/event.rb
new file mode 100644
index 000000000..685c41104
--- /dev/null
+++ b/lib/sessions/event.rb
@@ -0,0 +1,18 @@
+class Sessions::Event
+ include ApplicationLib
+
+ def self.run(event, data, session, client_id)
+ adapter = "Sessions::Event::#{event.to_classname}"
+ begin
+ backend = load_adapter(adapter)
+ rescue => e
+ return { error: "No such event #{event}" }
+ end
+
+ ActiveRecord::Base.establish_connection
+ result = backend.run(data, session, client_id)
+ ActiveRecord::Base.remove_connection
+ result
+ end
+
+end
\ No newline at end of file
diff --git a/lib/sessions/event/chat_agent_state.rb b/lib/sessions/event/chat_agent_state.rb
new file mode 100644
index 000000000..f63900a90
--- /dev/null
+++ b/lib/sessions/event/chat_agent_state.rb
@@ -0,0 +1,38 @@
+class Sessions::Event::ChatAgentState
+
+ def self.run(data, session, _client_id)
+
+ # check if feature is enabled
+ if !Setting.get('chat')
+ return {
+ event: 'chat_agent_state',
+ data: {
+ state: 'chat_disabled',
+ },
+ }
+ end
+
+ # only agents can do this
+ chat_id = 1
+ chat = Chat.find_by(id: chat_id)
+ if !session['id']
+ return {
+ event: 'chat_agent_state',
+ data: {
+ state: 'failed',
+ message: 'No such user_id.'
+ },
+ }
+ end
+
+ Chat::Agent.state(session['id'], data['data']['active'])
+
+ {
+ event: 'chat_agent_state',
+ data: {
+ state: 'ok',
+ active: data['data']['active'],
+ },
+ }
+ end
+end
diff --git a/lib/sessions/event/chat_session_close.rb b/lib/sessions/event/chat_session_close.rb
new file mode 100644
index 000000000..6a5a0a535
--- /dev/null
+++ b/lib/sessions/event/chat_session_close.rb
@@ -0,0 +1,46 @@
+class Sessions::Event::ChatSessionClose
+
+ def self.run(data, _session, _client_id)
+
+ # check if feature is enabled
+ if !Setting.get('chat')
+ return {
+ event: 'chat_status_close',
+ data: {
+ state: 'chat_disabled',
+ },
+ }
+ end
+
+ if !data['data'] || !data['data']['session_id']
+ return {
+ event: 'chat_status_close',
+ data: {
+ state: 'Need session_id.',
+ },
+ }
+ end
+
+ chat_session = Chat::Session.find_by(session_id: data['data']['session_id'])
+ if !chat_session
+ return {
+ event: 'chat_status_close',
+ data: {
+ state: "No such session id #{data['data']['session_id']}",
+ },
+ }
+ end
+
+ chat_session.state = 'closed'
+ chat_session.save
+
+ # return new session
+ {
+ event: 'chat_status_close',
+ data: {
+ state: 'ok',
+ session_id: chat_session.session_id,
+ },
+ }
+ end
+end
diff --git a/lib/sessions/event/chat_session_init.rb b/lib/sessions/event/chat_session_init.rb
new file mode 100644
index 000000000..f2bd37068
--- /dev/null
+++ b/lib/sessions/event/chat_session_init.rb
@@ -0,0 +1,55 @@
+class Sessions::Event::ChatSessionInit
+
+ def self.run(data, _session, client_id)
+
+ # check if feature is enabled
+ if !Setting.get('chat')
+ return {
+ event: 'chat_session_init',
+ data: {
+ state: 'chat_disabled',
+ },
+ }
+ end
+
+ chat_id = 1
+ chat = Chat.find_by(id: chat_id)
+ if !chat
+ return {
+ event: 'chat_session_init',
+ data: {
+ state: 'no_such_chat',
+ },
+ }
+ end
+
+ # create chat session
+ chat_session = Chat::Session.create(
+ chat_id: chat_id,
+ name: '',
+ state: 'waiting',
+ preferences: {
+ participants: [client_id],
+ },
+ )
+
+ # send update to agents
+ User.where(active: true).each {|user|
+ data = {
+ event: 'chat_status_agent',
+ data: Chat.agent_state(user.id),
+ }
+ Sessions.send_to(user.id, data)
+ }
+
+ # return new session
+ {
+ event: 'chat_session_init',
+ data: {
+ state: 'queue',
+ position: Chat.waiting_chat_count,
+ session_id: chat_session.session_id,
+ },
+ }
+ end
+end
diff --git a/lib/sessions/event/chat_session_message.rb b/lib/sessions/event/chat_session_message.rb
new file mode 100644
index 000000000..f410943cd
--- /dev/null
+++ b/lib/sessions/event/chat_session_message.rb
@@ -0,0 +1,68 @@
+class Sessions::Event::ChatSessionMessage
+
+ def self.run(data, session, client_id)
+
+ # check if feature is enabled
+ if !Setting.get('chat')
+ return {
+ event: 'chat_session_message',
+ data: {
+ state: 'chat_disabled',
+ },
+ }
+ end
+
+ if !data['data'] || !data['data']['session_id']
+ return {
+ event: 'chat_session_message',
+ data: {
+ state: 'Need session_id.',
+ },
+ }
+ end
+
+ chat_session = Chat::Session.find_by(session_id: data['data']['session_id'])
+ if !chat_session
+ return {
+ event: 'chat_session_message',
+ data: {
+ state: "No such session id #{data['data']['session_id']}",
+ },
+ }
+ end
+
+ user_id = nil
+ if session
+ user_id = session['id']
+ end
+ chat_message = Chat::Message.create(
+ chat_session_id: chat_session.id,
+ content: data['data']['content'],
+ created_by_id: user_id,
+ )
+ message = {
+ event: 'chat_session_message',
+ data: {
+ session_id: chat_session.session_id,
+ message: chat_message,
+ },
+ }
+
+ # send to participents
+ chat_session.preferences[:participants].each {|local_client_id|
+ next if local_client_id == client_id
+ Sessions.send(local_client_id, message)
+ }
+
+ # send chat_session_init to agent
+ {
+ event: 'chat_session_message',
+ data: {
+ session_id: chat_session.session_id,
+ message: chat_message,
+ self_written: true,
+ },
+ }
+
+ end
+end
diff --git a/lib/sessions/event/chat_session_start.rb b/lib/sessions/event/chat_session_start.rb
new file mode 100644
index 000000000..c7880e259
--- /dev/null
+++ b/lib/sessions/event/chat_session_start.rb
@@ -0,0 +1,60 @@
+class Sessions::Event::ChatSessionStart
+
+ def self.run(data, session, client_id)
+
+ # check if feature is enabled
+ if !Setting.get('chat')
+ return {
+ event: 'chat_session_start',
+ data: {
+ state: 'chat_disabled',
+ },
+ }
+ end
+
+ # find first in waiting list
+ chat_session = Chat::Session.where(state: 'waiting').order('created_at ASC').first
+ if !chat_session
+ return {
+ event: 'chat_session_start',
+ data: {
+ state: 'failed',
+ message: 'No session available.',
+ },
+ }
+ end
+ chat_session.user_id = session['id']
+ chat_session.state = 'running'
+ chat_session.preferences[:participants].push client_id
+ chat_session.save
+
+ # send chat_session_init to client
+ chat_user = User.find(chat_session.user_id)
+ user = {
+ name: chat_user.fullname,
+ avatar: chat_user.image,
+ }
+ data = {
+ event: 'chat_session_start',
+ data: {
+ state: 'ok',
+ agent: user,
+ session_id: chat_session.session_id,
+ },
+ }
+
+ chat_session.preferences[:participants].each {|local_client_id|
+ next if local_client_id == client_id
+ Sessions.send(local_client_id, data)
+ }
+
+ # send chat_session_init to agent
+ {
+ event: 'chat_session_start',
+ data: {
+ state: 'ok',
+ session: chat_session,
+ },
+ }
+ end
+end
diff --git a/lib/sessions/event/chat_session_typing.rb b/lib/sessions/event/chat_session_typing.rb
new file mode 100644
index 000000000..660818c61
--- /dev/null
+++ b/lib/sessions/event/chat_session_typing.rb
@@ -0,0 +1,61 @@
+class Sessions::Event::ChatSessionTyping
+
+ def self.run(data, session, client_id)
+
+ # check if feature is enabled
+ if !Setting.get('chat')
+ return {
+ event: 'chat_session_typing',
+ data: {
+ state: 'chat_disabled',
+ },
+ }
+ end
+
+ if !data['data'] || !data['data']['session_id']
+ return {
+ event: 'chat_session_typing',
+ data: {
+ state: 'Need session_id.',
+ },
+ }
+ end
+
+ chat_session = Chat::Session.find_by(session_id: data['data']['session_id'])
+ if !chat_session
+ return {
+ event: 'chat_session_typing',
+ data: {
+ state: "No such session id #{data['data']['session_id']}",
+ },
+ }
+ end
+
+ user_id = nil
+ if session
+ user_id = session['id']
+ end
+ message = {
+ event: 'chat_session_typing',
+ data: {
+ session_id: chat_session.session_id,
+ user_id: user_id,
+ },
+ }
+
+ # send to participents
+ chat_session.preferences[:participants].each {|local_client_id|
+ next if local_client_id == client_id
+ Sessions.send(local_client_id, message)
+ }
+
+ # send chat_session_init to agent
+ {
+ event: 'chat_session_typing',
+ data: {
+ session_id: chat_session.session_id,
+ self_written: true,
+ },
+ }
+ end
+end
diff --git a/lib/sessions/event/chat_status.rb b/lib/sessions/event/chat_status.rb
new file mode 100644
index 000000000..40576a53c
--- /dev/null
+++ b/lib/sessions/event/chat_status.rb
@@ -0,0 +1,31 @@
+class Sessions::Event::ChatStatus
+
+ def self.run(_data, _session, _client_id)
+
+ # check if feature is enabled
+ if !Setting.get('chat')
+ return {
+ event: 'chat_status',
+ data: {
+ state: 'chat_disabled',
+ },
+ }
+ end
+
+ chat_id = 1
+ chat = Chat.find_by(id: chat_id)
+ if !chat
+ return {
+ event: 'chat_status',
+ data: {
+ state: 'no_such_chat',
+ },
+ }
+ end
+
+ {
+ event: 'chat_status',
+ data: chat.state,
+ }
+ end
+end
diff --git a/lib/sessions/event/chat_status_agent.rb b/lib/sessions/event/chat_status_agent.rb
new file mode 100644
index 000000000..8890fddd7
--- /dev/null
+++ b/lib/sessions/event/chat_status_agent.rb
@@ -0,0 +1,27 @@
+class Sessions::Event::ChatStatusAgent
+
+ def self.run(_data, session, _client_id)
+
+ # check if user has permissions
+
+ # check if feature is enabled
+ if !Setting.get('chat')
+ return {
+ event: 'chat_status_agent',
+ data: {
+ state: 'chat_disabled',
+ },
+ }
+ end
+
+ # renew timestamps
+ state = Chat::Agent.state(session['id'])
+ Chat::Agent.state(session['id'], state)
+
+ {
+ event: 'chat_status_agent',
+ data: Chat.agent_state(session['id']),
+ }
+ end
+
+end
diff --git a/public/assets/chat/chat.coffee b/public/assets/chat/chat.coffee
index 27338e4f7..2a5e11ba8 100644
--- a/public/assets/chat/chat.coffee
+++ b/public/assets/chat/chat.coffee
@@ -51,6 +51,8 @@ do($ = window.jQuery, window) ->
input: @onInput
).autoGrow { extraLine: false }
+ @session_id = undefined
+
if !window.WebSocket
console.log('Zammad Chat: Browser not supported')
return
@@ -61,7 +63,7 @@ do($ = window.jQuery, window) ->
@ws.onopen = =>
console.log('ws connected')
- @send "chat_status"
+ @send 'chat_status'
@ws.onmessage = @onWebSocketMessage
@@ -76,40 +78,52 @@ do($ = window.jQuery, window) ->
event.preventDefault()
@sendMessage()
- send: (action, data) =>
+ send: (event, data) =>
+ console.log 'debug', 'ws:send', event, data
pipe = JSON.stringify
- action: action
+ event: event
data: data
-
@ws.send pipe
onWebSocketMessage: (e) =>
- pipe = JSON.parse( e.data )
- console.log 'debug', 'ws:onmessage', pipe
+ pipes = JSON.parse( e.data )
+ console.log 'debug', 'ws:onmessage', pipes
- switch pipe.action
- when 'chat_message'
- @receiveMessage pipe.data
- when 'chat_typing_start'
- @onAgentTypingStart()
- when 'chat_typing_end'
- @onAgentTypingEnd()
- when 'chat_init'
- switch pipe.data.state
- when 'ok'
- @onConnectionEstablished pipe.data.agent
- when 'queue'
- @onQueue pipe.data.position
- when 'chat_status'
- switch pipe.data.state
- when 'ok'
- @onReady()
- when 'offline'
- console.log 'Zammad Chat: No agent online'
- when 'chat_disabled'
- console.log 'Zammad Chat: Chat is disabled'
- when 'no_seats_available'
- console.log 'Zammad Chat: Too many clients in queue. Clients in queue: ', pipe.data.queue
+ for pipe in pipes
+ switch pipe.event
+ when 'chat_session_message'
+ return if pipe.data.self_written
+ @receiveMessage pipe.data
+ when 'chat_session_typing'
+ return if pipe.data.self_written
+ @onAgentTypingStart()
+ if @stopTypingId
+ clearTimeout(@stopTypingId)
+ delay = =>
+ @onAgentTypingEnd()
+ @stopTypingId = setTimeout(delay, 3000)
+ when 'chat_session_start'
+ switch pipe.data.state
+ when 'ok'
+ @onConnectionEstablished pipe.data.agent
+ when 'chat_session_init'
+ switch pipe.data.state
+ when 'ok'
+ @onConnectionEstablished pipe.data.agent
+ when 'queue'
+ @onQueue pipe.data.position
+ @session_id = pipe.data.session_id
+ when 'chat_status'
+ switch pipe.data.state
+ when 'online'
+ @onReady()
+ console.log 'Zammad Chat: ready'
+ when 'offline'
+ console.log 'Zammad Chat: No agent online'
+ when 'chat_disabled'
+ console.log 'Zammad Chat: Chat is disabled'
+ when 'no_seats_available'
+ console.log 'Zammad Chat: Too many clients in queue. Clients in queue: ', pipe.data.queue
onReady: =>
@show() if @options.show
@@ -119,22 +133,22 @@ do($ = window.jQuery, window) ->
@el.find('.zammad-chat-message--unread')
.removeClass 'zammad-chat-message--unread'
- clearTimeout(@inputTimeout) if @inputTimeout
-
- # fire typingEnd after 5 seconds
- @inputTimeout = setTimeout @onTypingEnd, 5000
-
- @onTypingStart() if @isTyping
+ @onTypingStart()
onTypingStart: ->
+
+ clearTimeout(@isTypingTimeout) if @isTypingTimeout
+
+ # fire typingEnd after 5 seconds
+ @isTypingTimeout = setTimeout @onTypingEnd, 1500
+
# send typing start event
- @isTyping = true
- @send 'typing_start'
+ if !@isTyping
+ @isTyping = true
+ @send 'chat_session_typing', {session_id: @session_id}
onTypingEnd: =>
- # send typing end event
@isTyping = false
- @send 'typing_end'
onSubmit: (event) =>
event.preventDefault()
@@ -167,9 +181,10 @@ do($ = window.jQuery, window) ->
@isTyping = false
# send message event
- @send 'message',
- body: message
+ @send 'chat_session_message',
+ content: message
id: @_messageCount
+ session_id: @session_id
receiveMessage: (data) =>
# hide writing indicator
@@ -180,7 +195,7 @@ do($ = window.jQuery, window) ->
@lastAddedType = 'message--agent'
unread = document.hidden ? " zammad-chat-message--unread" : ""
@el.find('.zammad-chat-body').append @view('message')
- message: data.body
+ message: data.message.content
id: data.id
from: 'agent'
@scrollToBottom()
@@ -275,7 +290,7 @@ do($ = window.jQuery, window) ->
@el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'))
connect: ->
- @send('chat_init')
+ @send('chat_session_init')
reconnect: =>
# set status to connecting
diff --git a/public/assets/chat/chat.js b/public/assets/chat/chat.js
index 6ef8a0949..d6b5109d6 100644
--- a/public/assets/chat/chat.js
+++ b/public/assets/chat/chat.js
@@ -1,66 +1,3 @@
-if (!window.zammadChatTemplates) {
- window.zammadChatTemplates = {};
-}
-window.zammadChatTemplates["agent"] = function (__obj) {
- if (!__obj) __obj = {};
- var __out = [], __capture = function(callback) {
- var out = __out, result;
- __out = [];
- callback.call(this);
- result = __out.join('');
- __out = out;
- return __safe(result);
- }, __sanitize = function(value) {
- if (value && value.ecoSafe) {
- return value;
- } else if (typeof value !== 'undefined' && value != null) {
- return __escape(value);
- } else {
- return '';
- }
- }, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
- __safe = __obj.safe = function(value) {
- if (value && value.ecoSafe) {
- return value;
- } else {
- if (!(typeof value !== 'undefined' && value != null)) value = '';
- var result = new String(value);
- result.ecoSafe = true;
- return result;
- }
- };
- if (!__escape) {
- __escape = __obj.escape = function(value) {
- return ('' + value)
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"');
- };
- }
- (function() {
- (function() {
- __out.push('\n\n ');
-
- __out.push(__sanitize(this.agent.name));
-
- __out.push(' ');
-
- __out.push(this.agentPhrase);
-
- __out.push('\n');
-
- }).call(this);
-
- }).call(__obj);
- __obj.safe = __objSafe, __obj.escape = __escape;
- return __out.join('');
-};
-
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
(function($, window) {
@@ -151,6 +88,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}).autoGrow({
extraLine: false
});
+ this.session_id = void 0;
if (!window.WebSocket) {
console.log('Zammad Chat: Browser not supported');
return;
@@ -161,7 +99,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.ws.onopen = (function(_this) {
return function() {
console.log('ws connected');
- return _this.send("chat_status");
+ return _this.send('chat_status');
};
})(this);
this.ws.onmessage = this.onWebSocketMessage;
@@ -184,45 +122,76 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}
};
- ZammadChat.prototype.send = function(action, data) {
+ ZammadChat.prototype.send = function(event, data) {
var pipe;
+ console.log('debug', 'ws:send', event, data);
pipe = JSON.stringify({
- action: action,
+ event: event,
data: data
});
return this.ws.send(pipe);
};
ZammadChat.prototype.onWebSocketMessage = function(e) {
- var pipe;
- pipe = JSON.parse(e.data);
- console.log('debug', 'ws:onmessage', pipe);
- switch (pipe.action) {
- case 'chat_message':
- return this.receiveMessage(pipe.data);
- case 'chat_typing_start':
- return this.onAgentTypingStart();
- case 'chat_typing_end':
- return this.onAgentTypingEnd();
- case 'chat_init':
- switch (pipe.data.state) {
- case 'ok':
- return this.onConnectionEstablished(pipe.data.agent);
- case 'queue':
- return this.onQueue(pipe.data.position);
- }
- break;
- case 'chat_status':
- switch (pipe.data.state) {
- case 'ok':
- return this.onReady();
- case 'offline':
- return console.log('Zammad Chat: No agent online');
- case 'chat_disabled':
- return console.log('Zammad Chat: Chat is disabled');
- case 'no_seats_available':
- return console.log('Zammad Chat: Too many clients in queue. Clients in queue: ', pipe.data.queue);
- }
+ var delay, i, len, pipe, pipes;
+ pipes = JSON.parse(e.data);
+ console.log('debug', 'ws:onmessage', pipes);
+ for (i = 0, len = pipes.length; i < len; i++) {
+ pipe = pipes[i];
+ switch (pipe.event) {
+ case 'chat_session_message':
+ if (pipe.data.self_written) {
+ return;
+ }
+ this.receiveMessage(pipe.data);
+ break;
+ case 'chat_session_typing':
+ if (pipe.data.self_written) {
+ return;
+ }
+ this.onAgentTypingStart();
+ if (this.stopTypingId) {
+ clearTimeout(this.stopTypingId);
+ }
+ delay = (function(_this) {
+ return function() {
+ return _this.onAgentTypingEnd();
+ };
+ })(this);
+ this.stopTypingId = setTimeout(delay, 3000);
+ break;
+ case 'chat_session_start':
+ switch (pipe.data.state) {
+ case 'ok':
+ this.onConnectionEstablished(pipe.data.agent);
+ }
+ break;
+ case 'chat_session_init':
+ switch (pipe.data.state) {
+ case 'ok':
+ this.onConnectionEstablished(pipe.data.agent);
+ break;
+ case 'queue':
+ this.onQueue(pipe.data.position);
+ this.session_id = pipe.data.session_id;
+ }
+ break;
+ case 'chat_status':
+ switch (pipe.data.state) {
+ case 'online':
+ this.onReady();
+ console.log('Zammad Chat: ready');
+ break;
+ case 'offline':
+ console.log('Zammad Chat: No agent online');
+ break;
+ case 'chat_disabled':
+ console.log('Zammad Chat: Chat is disabled');
+ break;
+ case 'no_seats_available':
+ console.log('Zammad Chat: Too many clients in queue. Clients in queue: ', pipe.data.queue);
+ }
+ }
}
};
@@ -234,23 +203,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
ZammadChat.prototype.onInput = function() {
this.el.find('.zammad-chat-message--unread').removeClass('zammad-chat-message--unread');
- if (this.inputTimeout) {
- clearTimeout(this.inputTimeout);
- }
- this.inputTimeout = setTimeout(this.onTypingEnd, 5000);
- if (this.isTyping) {
- return this.onTypingStart();
- }
+ return this.onTypingStart();
};
ZammadChat.prototype.onTypingStart = function() {
- this.isTyping = true;
- return this.send('typing_start');
+ if (this.isTypingTimeout) {
+ clearTimeout(this.isTypingTimeout);
+ }
+ this.isTypingTimeout = setTimeout(this.onTypingEnd, 1500);
+ if (!this.isTyping) {
+ this.isTyping = true;
+ return this.send('chat_session_typing', {
+ session_id: this.session_id
+ });
+ }
};
ZammadChat.prototype.onTypingEnd = function() {
- this.isTyping = false;
- return this.send('typing_end');
+ return this.isTyping = false;
};
ZammadChat.prototype.onSubmit = function(event) {
@@ -280,9 +250,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.el.find('.zammad-chat-input').val('');
this.scrollToBottom();
this.isTyping = false;
- return this.send('message', {
- body: message,
- id: this._messageCount
+ return this.send('chat_session_message', {
+ content: message,
+ id: this._messageCount,
+ session_id: this.session_id
});
};
@@ -295,7 +266,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
" zammad-chat-message--unread": ""
};
this.el.find('.zammad-chat-body').append(this.view('message')({
- message: data.body,
+ message: data.message.content,
id: data.id,
from: 'agent'
}));
@@ -404,7 +375,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
};
ZammadChat.prototype.connect = function() {
- return this.send('chat_init');
+ return this.send('chat_session_init');
};
ZammadChat.prototype.reconnect = function() {
@@ -456,6 +427,69 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
});
})(window.jQuery, window);
+if (!window.zammadChatTemplates) {
+ window.zammadChatTemplates = {};
+}
+window.zammadChatTemplates["agent"] = function (__obj) {
+ if (!__obj) __obj = {};
+ var __out = [], __capture = function(callback) {
+ var out = __out, result;
+ __out = [];
+ callback.call(this);
+ result = __out.join('');
+ __out = out;
+ return __safe(result);
+ }, __sanitize = function(value) {
+ if (value && value.ecoSafe) {
+ return value;
+ } else if (typeof value !== 'undefined' && value != null) {
+ return __escape(value);
+ } else {
+ return '';
+ }
+ }, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
+ __safe = __obj.safe = function(value) {
+ if (value && value.ecoSafe) {
+ return value;
+ } else {
+ if (!(typeof value !== 'undefined' && value != null)) value = '';
+ var result = new String(value);
+ result.ecoSafe = true;
+ return result;
+ }
+ };
+ if (!__escape) {
+ __escape = __obj.escape = function(value) {
+ return ('' + value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+ };
+ }
+ (function() {
+ (function() {
+ __out.push('\n\n ');
+
+ __out.push(__sanitize(this.agent.name));
+
+ __out.push(' ');
+
+ __out.push(this.agentPhrase);
+
+ __out.push('\n');
+
+ }).call(this);
+
+ }).call(__obj);
+ __obj.safe = __objSafe, __obj.escape = __escape;
+ return __out.join('');
+};
+
/*!
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
diff --git a/public/assets/chat/chat.min.js b/public/assets/chat/chat.min.js
index 898670000..ec3d2050f 100644
--- a/public/assets/chat/chat.min.js
+++ b/public/assets/chat/chat.min.js
@@ -1 +1 @@
-window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n\n '),n.push(a(this.agent.name)),n.push(" "),n.push(this.agentPhrase),n.push("\n")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")};var bind=function(t,e){return function(){return t.apply(e,arguments)}};!function(t,e){var n;return n=function(){function n(n,a){this.setAgentOnlineState=bind(this.setAgentOnlineState,this),this.onConnectionEstablished=bind(this.onConnectionEstablished,this),this.onConnectionReestablished=bind(this.onConnectionReestablished,this),this.reconnect=bind(this.reconnect,this),this.onAgentTypingEnd=bind(this.onAgentTypingEnd,this),this.onAgentTypingStart=bind(this.onAgentTypingStart,this),this.onQueue=bind(this.onQueue,this),this.onCloseAnimationEnd=bind(this.onCloseAnimationEnd,this),this.onOpenAnimationEnd=bind(this.onOpenAnimationEnd,this),this.toggle=bind(this.toggle,this),this.receiveMessage=bind(this.receiveMessage,this),this.onSubmit=bind(this.onSubmit,this),this.onTypingEnd=bind(this.onTypingEnd,this),this.onInput=bind(this.onInput,this),this.onReady=bind(this.onReady,this),this.onWebSocketMessage=bind(this.onWebSocketMessage,this),this.send=bind(this.send,this),this.checkForEnter=bind(this.checkForEnter,this),this.view=bind(this.view,this),this.T=bind(this.T,this);var s;return this.options=t.extend({},this.defaults,a),this.el=t(this.view("chat")(this.options)),this.options.target.append(this.el),this.setAgentOnlineState(this.isOnline),this.el.find(".zammad-chat-header").click(this.toggle),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-input").on({keydown:this.checkForEnter,input:this.onInput}).autoGrow({extraLine:!1}),e.WebSocket?(s="ws://localhost:6042",this.ws=new e.WebSocket(s),console.log("Connecting to "+s),this.ws.onopen=function(t){return function(){return console.log("ws connected"),t.send("chat_status")}}(this),this.ws.onmessage=this.onWebSocketMessage,this.ws.onclose=function(t){return function(t){return console.log("debug","close websocket connection")}}(this),void(this.ws.onerror=function(t){return function(t){return console.log("debug","ws:onerror",t)}}(this))):void console.log("Zammad Chat: Browser not supported")}return n.prototype.defaults={invitationPhrase:"Chat with us!",agentPhrase:" is helping you",show:!0,target:t("body")},n.prototype._messageCount=0,n.prototype.isOpen=!1,n.prototype.blinkOnlineInterval=null,n.prototype.stopBlinOnlineStateTimeout=null,n.prototype.showTimeEveryXMinutes=1,n.prototype.lastTimestamp=null,n.prototype.lastAddedType=null,n.prototype.inputTimeout=null,n.prototype.isTyping=!1,n.prototype.isOnline=!0,n.prototype.strings={Online:"Online",Offline:"Offline",Connecting:"Connecting","Connection re-established":"Connection re-established",Today:"Today"},n.prototype.T=function(t){return this.strings[t]},n.prototype.view=function(t){return function(n){return function(a){return a||(a={}),a.T=n.T,e.zammadChatTemplates[t](a)}}(this)},n.prototype.checkForEnter=function(t){return t.shiftKey||13!==t.keyCode?void 0:(t.preventDefault(),this.sendMessage())},n.prototype.send=function(t,e){var n;return n=JSON.stringify({action:t,data:e}),this.ws.send(n)},n.prototype.onWebSocketMessage=function(t){var e;switch(e=JSON.parse(t.data),console.log("debug","ws:onmessage",e),e.action){case"chat_message":return this.receiveMessage(e.data);case"chat_typing_start":return this.onAgentTypingStart();case"chat_typing_end":return this.onAgentTypingEnd();case"chat_init":switch(e.data.state){case"ok":return this.onConnectionEstablished(e.data.agent);case"queue":return this.onQueue(e.data.position)}break;case"chat_status":switch(e.data.state){case"ok":return this.onReady();case"offline":return console.log("Zammad Chat: No agent online");case"chat_disabled":return console.log("Zammad Chat: Chat is disabled");case"no_seats_available":return console.log("Zammad Chat: Too many clients in queue. Clients in queue: ",e.data.queue)}}},n.prototype.onReady=function(){return this.options.show?this.show():void 0},n.prototype.onInput=function(){return this.el.find(".zammad-chat-message--unread").removeClass("zammad-chat-message--unread"),this.inputTimeout&&clearTimeout(this.inputTimeout),this.inputTimeout=setTimeout(this.onTypingEnd,5e3),this.isTyping?this.onTypingStart():void 0},n.prototype.onTypingStart=function(){return this.isTyping=!0,this.send("typing_start")},n.prototype.onTypingEnd=function(){return this.isTyping=!1,this.send("typing_end")},n.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},n.prototype.sendMessage=function(){var t,e;return(t=this.el.find(".zammad-chat-input").val())?(e=this.view("message")({message:t,from:"customer",id:this._messageCount++}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").size()?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.el.find(".zammad-chat-input").val(""),this.scrollToBottom(),this.isTyping=!1,this.send("message",{body:t,id:this._messageCount})):void 0},n.prototype.receiveMessage=function(t){var e,n;return this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.lastAddedType="message--agent",n=null!=(e=document.hidden)?e:{" zammad-chat-message--unread":""},this.el.find(".zammad-chat-body").append(this.view("message")({message:t.body,id:t.id,from:"agent"})),this.scrollToBottom()},n.prototype.toggle=function(){return this.isOpen?this.close():this.open()},n.prototype.open=function(){return this.showLoader(),this.el.addClass("zammad-chat-is-open").animate({bottom:0},500,this.onOpenAnimationEnd)},n.prototype.onOpenAnimationEnd=function(){return this.isOpen=!0,this.connect()},n.prototype.close=function(){var t;return t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-t},500,this.onCloseAnimationEnd)},n.prototype.onCloseAnimationEnd=function(){return this.el.removeClass("zammad-chat-is-open"),this.disconnect(),this.isOpen=!1},n.prototype.hide=function(){return this.el.removeClass("zammad-chat-is-visible")},n.prototype.show=function(){var t;return this.el.addClass("zammad-chat-is-visible"),t=this.el.outerHeight()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t)},n.prototype.onQueue=function(t){return console.log("onQueue",t),this.inQueue=!0,this.el.find(".zammad-chat-body").html(this.view("waiting")({position:t}))},n.prototype.onAgentTypingStart=function(){return this.el.find(".zammad-chat-message--typing").size()?void 0:(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.scrollToBottom())},n.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},n.prototype.maybeAddTimestamp=function(){var t,e,n;return n=Date.now(),!this.lastTimestamp||n-this.lastTimestamp>6e4*this.showTimeEveryXMinutes?(t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=n):(this.addStatus(t,e),this.lastTimestamp=n,this.lastAddedType="timestamp")):void 0},n.prototype.updateLastTimestamp=function(t,e){return this.el.find(".zammad-chat-body").find(".zammad-chat-status").last().replaceWith(this.view("status")({label:t,time:e}))},n.prototype.addStatus=function(t,e){return this.el.find(".zammad-chat-body").append(this.view("status")({label:t,time:e}))},n.prototype.scrollToBottom=function(){return this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight"))},n.prototype.connect=function(){return this.send("chat_init")},n.prototype.reconnect=function(){return this.lastAddedType="status",this.el.find(".zammad-chat-agent-status").attr("data-status","connecting").text(this.T("Connecting")),this.addStatus(this.T("Connection lost"))},n.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.el.find(".zammad-chat-agent-status").attr("data-status","online").text(this.T("Online")),this.addStatus(this.T("Connection re-established"))},n.prototype.disconnect=function(){return this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden")},n.prototype.onConnectionEstablished=function(t){return this.inQueue=!1,this.agent=t,this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:t})),this.el.find(".zammad-chat-body").empty(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-input").focus()},n.prototype.showLoader=function(){return this.el.find(".zammad-chat-body").html(this.view("loader")())},n.prototype.setAgentOnlineState=function(t){return this.isOnline=t,this.el.find(".zammad-chat-agent-status").toggleClass("zammad-chat-is-online",t).text(t?this.T("Online"):this.T("Offline"))},n}(),t(document).ready(function(){return e.zammadChat=new n})}(window.jQuery,window),jQuery.fn.autoGrow=function(t){return this.each(function(){var e=jQuery.extend({extraLine:!0},t),n=function(t){return jQuery(t).after(''),jQuery(t).next(".autogrow-textarea-mirror")[0]},a=function(t){i.innerHTML=String(t.value).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">").replace(/ /g," ").replace(/\n/g,"
")+(e.extraLine?".
.":""),jQuery(t).height()!=jQuery(i).height()&&jQuery(t).height(jQuery(i).height())},s=function(){a(this)},i=n(this);i.style.display="none",i.style.wordWrap="break-word",i.style.whiteSpace="normal",i.style.padding=jQuery(this).css("paddingTop")+" "+jQuery(this).css("paddingRight")+" "+jQuery(this).css("paddingBottom")+" "+jQuery(this).css("paddingLeft"),i.style.width=jQuery(this).css("width"),i.style.fontFamily=jQuery(this).css("font-family"),i.style.fontSize=jQuery(this).css("font-size"),i.style.lineHeight=jQuery(this).css("line-height"),this.style.overflow="hidden",this.style.minHeight=this.rows+"em",this.onkeyup=s,a(this)})},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.chat=function(t){t||(t={});var e,n=[],a=t.safe,s=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},s||(s=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('')}).call(this)}.call(t),t.safe=a,t.escape=s,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n \n \n \n \n \n '),n.push(a(this.T("Connecting"))),n.push("\n
")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n '),n.push(this.message),n.push("\n
")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push(''),n.push(a(this.label)),n.push(""),n.push(a(this.time)),n.push("
")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e,n=[],a=t.safe,s=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},s||(s=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n \n \n \n \n \n \n \n
')}).call(this)}.call(t),t.safe=a,t.escape=s,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n
\n \n \n \n \n \n Leider sind gerade alle Mitarbeiter belegt.
\n Warteliste-Position: '),n.push(a(this.position)),n.push("\n
\n
")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")};
\ No newline at end of file
+var bind=function(t,e){return function(){return t.apply(e,arguments)}};!function(t,e){var n;return n=function(){function n(n,a){this.setAgentOnlineState=bind(this.setAgentOnlineState,this),this.onConnectionEstablished=bind(this.onConnectionEstablished,this),this.onConnectionReestablished=bind(this.onConnectionReestablished,this),this.reconnect=bind(this.reconnect,this),this.onAgentTypingEnd=bind(this.onAgentTypingEnd,this),this.onAgentTypingStart=bind(this.onAgentTypingStart,this),this.onQueue=bind(this.onQueue,this),this.onCloseAnimationEnd=bind(this.onCloseAnimationEnd,this),this.onOpenAnimationEnd=bind(this.onOpenAnimationEnd,this),this.toggle=bind(this.toggle,this),this.receiveMessage=bind(this.receiveMessage,this),this.onSubmit=bind(this.onSubmit,this),this.onTypingEnd=bind(this.onTypingEnd,this),this.onInput=bind(this.onInput,this),this.onReady=bind(this.onReady,this),this.onWebSocketMessage=bind(this.onWebSocketMessage,this),this.send=bind(this.send,this),this.checkForEnter=bind(this.checkForEnter,this),this.view=bind(this.view,this),this.T=bind(this.T,this);var s;return this.options=t.extend({},this.defaults,a),this.el=t(this.view("chat")(this.options)),this.options.target.append(this.el),this.setAgentOnlineState(this.isOnline),this.el.find(".zammad-chat-header").click(this.toggle),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.el.find(".zammad-chat-input").on({keydown:this.checkForEnter,input:this.onInput}).autoGrow({extraLine:!1}),this.session_id=void 0,e.WebSocket?(s="ws://localhost:6042",this.ws=new e.WebSocket(s),console.log("Connecting to "+s),this.ws.onopen=function(t){return function(){return console.log("ws connected"),t.send("chat_status")}}(this),this.ws.onmessage=this.onWebSocketMessage,this.ws.onclose=function(t){return function(t){return console.log("debug","close websocket connection")}}(this),void(this.ws.onerror=function(t){return function(t){return console.log("debug","ws:onerror",t)}}(this))):void console.log("Zammad Chat: Browser not supported")}return n.prototype.defaults={invitationPhrase:"Chat with us!",agentPhrase:" is helping you",show:!0,target:t("body")},n.prototype._messageCount=0,n.prototype.isOpen=!1,n.prototype.blinkOnlineInterval=null,n.prototype.stopBlinOnlineStateTimeout=null,n.prototype.showTimeEveryXMinutes=1,n.prototype.lastTimestamp=null,n.prototype.lastAddedType=null,n.prototype.inputTimeout=null,n.prototype.isTyping=!1,n.prototype.isOnline=!0,n.prototype.strings={Online:"Online",Offline:"Offline",Connecting:"Connecting","Connection re-established":"Connection re-established",Today:"Today"},n.prototype.T=function(t){return this.strings[t]},n.prototype.view=function(t){return function(n){return function(a){return a||(a={}),a.T=n.T,e.zammadChatTemplates[t](a)}}(this)},n.prototype.checkForEnter=function(t){return t.shiftKey||13!==t.keyCode?void 0:(t.preventDefault(),this.sendMessage())},n.prototype.send=function(t,e){var n;return console.log("debug","ws:send",t,e),n=JSON.stringify({event:t,data:e}),this.ws.send(n)},n.prototype.onWebSocketMessage=function(t){var e,n,a,s,i;for(i=JSON.parse(t.data),console.log("debug","ws:onmessage",i),n=0,a=i.length;a>n;n++)switch(s=i[n],s.event){case"chat_session_message":if(s.data.self_written)return;this.receiveMessage(s.data);break;case"chat_session_typing":if(s.data.self_written)return;this.onAgentTypingStart(),this.stopTypingId&&clearTimeout(this.stopTypingId),e=function(t){return function(){return t.onAgentTypingEnd()}}(this),this.stopTypingId=setTimeout(e,3e3);break;case"chat_session_start":switch(s.data.state){case"ok":this.onConnectionEstablished(s.data.agent)}break;case"chat_session_init":switch(s.data.state){case"ok":this.onConnectionEstablished(s.data.agent);break;case"queue":this.onQueue(s.data.position),this.session_id=s.data.session_id}break;case"chat_status":switch(s.data.state){case"online":this.onReady(),console.log("Zammad Chat: ready");break;case"offline":console.log("Zammad Chat: No agent online");break;case"chat_disabled":console.log("Zammad Chat: Chat is disabled");break;case"no_seats_available":console.log("Zammad Chat: Too many clients in queue. Clients in queue: ",s.data.queue)}}},n.prototype.onReady=function(){return this.options.show?this.show():void 0},n.prototype.onInput=function(){return this.el.find(".zammad-chat-message--unread").removeClass("zammad-chat-message--unread"),this.onTypingStart()},n.prototype.onTypingStart=function(){return this.isTypingTimeout&&clearTimeout(this.isTypingTimeout),this.isTypingTimeout=setTimeout(this.onTypingEnd,1500),this.isTyping?void 0:(this.isTyping=!0,this.send("chat_session_typing",{session_id:this.session_id}))},n.prototype.onTypingEnd=function(){return this.isTyping=!1},n.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},n.prototype.sendMessage=function(){var t,e;return(t=this.el.find(".zammad-chat-input").val())?(e=this.view("message")({message:t,from:"customer",id:this._messageCount++}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").size()?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(e)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(e)),this.el.find(".zammad-chat-input").val(""),this.scrollToBottom(),this.isTyping=!1,this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.session_id})):void 0},n.prototype.receiveMessage=function(t){var e,n;return this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.lastAddedType="message--agent",n=null!=(e=document.hidden)?e:{" zammad-chat-message--unread":""},this.el.find(".zammad-chat-body").append(this.view("message")({message:t.message.content,id:t.id,from:"agent"})),this.scrollToBottom()},n.prototype.toggle=function(){return this.isOpen?this.close():this.open()},n.prototype.open=function(){return this.showLoader(),this.el.addClass("zammad-chat-is-open").animate({bottom:0},500,this.onOpenAnimationEnd)},n.prototype.onOpenAnimationEnd=function(){return this.isOpen=!0,this.connect()},n.prototype.close=function(){var t;return t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-t},500,this.onCloseAnimationEnd)},n.prototype.onCloseAnimationEnd=function(){return this.el.removeClass("zammad-chat-is-open"),this.disconnect(),this.isOpen=!1},n.prototype.hide=function(){return this.el.removeClass("zammad-chat-is-visible")},n.prototype.show=function(){var t;return this.el.addClass("zammad-chat-is-visible"),t=this.el.outerHeight()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t)},n.prototype.onQueue=function(t){return console.log("onQueue",t),this.inQueue=!0,this.el.find(".zammad-chat-body").html(this.view("waiting")({position:t}))},n.prototype.onAgentTypingStart=function(){return this.el.find(".zammad-chat-message--typing").size()?void 0:(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.scrollToBottom())},n.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},n.prototype.maybeAddTimestamp=function(){var t,e,n;return n=Date.now(),!this.lastTimestamp||n-this.lastTimestamp>6e4*this.showTimeEveryXMinutes?(t=this.T("Today"),e=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(t,e),this.lastTimestamp=n):(this.addStatus(t,e),this.lastTimestamp=n,this.lastAddedType="timestamp")):void 0},n.prototype.updateLastTimestamp=function(t,e){return this.el.find(".zammad-chat-body").find(".zammad-chat-status").last().replaceWith(this.view("status")({label:t,time:e}))},n.prototype.addStatus=function(t,e){return this.el.find(".zammad-chat-body").append(this.view("status")({label:t,time:e}))},n.prototype.scrollToBottom=function(){return this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight"))},n.prototype.connect=function(){return this.send("chat_session_init")},n.prototype.reconnect=function(){return this.lastAddedType="status",this.el.find(".zammad-chat-agent-status").attr("data-status","connecting").text(this.T("Connecting")),this.addStatus(this.T("Connection lost"))},n.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.el.find(".zammad-chat-agent-status").attr("data-status","online").text(this.T("Online")),this.addStatus(this.T("Connection re-established"))},n.prototype.disconnect=function(){return this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden")},n.prototype.onConnectionEstablished=function(t){return this.inQueue=!1,this.agent=t,this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:t})),this.el.find(".zammad-chat-body").empty(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-input").focus()},n.prototype.showLoader=function(){return this.el.find(".zammad-chat-body").html(this.view("loader")())},n.prototype.setAgentOnlineState=function(t){return this.isOnline=t,this.el.find(".zammad-chat-agent-status").toggleClass("zammad-chat-is-online",t).text(t?this.T("Online"):this.T("Offline"))},n}(),t(document).ready(function(){return e.zammadChat=new n})}(window.jQuery,window),window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n\n '),n.push(a(this.agent.name)),n.push(" "),n.push(this.agentPhrase),n.push("\n")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},jQuery.fn.autoGrow=function(t){return this.each(function(){var e=jQuery.extend({extraLine:!0},t),n=function(t){return jQuery(t).after(''),jQuery(t).next(".autogrow-textarea-mirror")[0]},a=function(t){i.innerHTML=String(t.value).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">").replace(/ /g," ").replace(/\n/g,"
")+(e.extraLine?".
.":""),jQuery(t).height()!=jQuery(i).height()&&jQuery(t).height(jQuery(i).height())},s=function(){a(this)},i=n(this);i.style.display="none",i.style.wordWrap="break-word",i.style.whiteSpace="normal",i.style.padding=jQuery(this).css("paddingTop")+" "+jQuery(this).css("paddingRight")+" "+jQuery(this).css("paddingBottom")+" "+jQuery(this).css("paddingLeft"),i.style.width=jQuery(this).css("width"),i.style.fontFamily=jQuery(this).css("font-family"),i.style.fontSize=jQuery(this).css("font-size"),i.style.lineHeight=jQuery(this).css("line-height"),this.style.overflow="hidden",this.style.minHeight=this.rows+"em",this.onkeyup=s,a(this)})},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.chat=function(t){t||(t={});var e,n=[],a=t.safe,s=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},s||(s=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('')}).call(this)}.call(t),t.safe=a,t.escape=s,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n \n \n \n \n \n '),n.push(a(this.T("Connecting"))),n.push("\n
")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n '),n.push(this.message),n.push("\n
")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push(''),n.push(a(this.label)),n.push(""),n.push(a(this.time)),n.push("
")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e,n=[],a=t.safe,s=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},s||(s=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n \n \n \n \n \n \n \n
')}).call(this)}.call(t),t.safe=a,t.escape=s,n.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e,n=[],a=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?i(t):""},s=t.safe,i=t.escape;return e=t.safe=function(t){if(t&&t.ecoSafe)return t;("undefined"==typeof t||null==t)&&(t="");var e=new String(t);return e.ecoSafe=!0,e},i||(i=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){n.push('\n
\n \n \n \n \n \n Leider sind gerade alle Mitarbeiter belegt.
\n Warteliste-Position: '),n.push(a(this.position)),n.push("\n
\n
")}).call(this)}.call(t),t.safe=s,t.escape=i,n.join("")};
\ No newline at end of file
diff --git a/public/assets/chat/index.html b/public/assets/chat/index.html
index a04ef6f00..7514b78f6 100644
--- a/public/assets/chat/index.html
+++ b/public/assets/chat/index.html
@@ -94,8 +94,11 @@
+
+
+