From 7831f1c1f8d481d623264c96e0cfce9f06f6844a Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Sat, 5 Dec 2015 20:41:14 +0100 Subject: [PATCH] Added browser tests for customer chat. --- .../app/controllers/_channel/chat.coffee | 4 +- .../app/controllers/_channel/form.coffee | 2 +- .../javascripts/app/controllers/chat.coffee | 16 +- .../app/controllers/navigation.coffee | 2 +- .../app/views/channel/chat.jst.eco | 4 +- .../app/views/channel/form.jst.eco | 4 +- .../app/views/navigation/menu.jst.eco | 4 +- app/models/chat/session.rb | 12 + .../observer/chat/leave/background_job.rb | 36 + app/models/setting.rb | 6 +- public/assets/chat/chat.coffee | 395 +++++--- public/assets/chat/chat.js | 922 ++++++++++++------ public/assets/chat/chat.min.js | 3 +- .../{timeout.eco => customer_timeout.eco} | 0 .../chat/views/waiting_list_timeout.eco | 7 + public/assets/chat/znuny.html | 22 +- script/websocket-server.rb | 1 + test/browser/chat_test.rb | 544 ++++++++--- 18 files changed, 1418 insertions(+), 566 deletions(-) create mode 100644 app/models/observer/chat/leave/background_job.rb rename public/assets/chat/views/{timeout.eco => customer_timeout.eco} (100%) create mode 100644 public/assets/chat/views/waiting_list_timeout.eco diff --git a/app/assets/javascripts/app/controllers/_channel/chat.coffee b/app/assets/javascripts/app/controllers/_channel/chat.coffee index e24caaa72..15c5d13c0 100644 --- a/app/assets/javascripts/app/controllers/_channel/chat.coffee +++ b/app/assets/javascripts/app/controllers/_channel/chat.coffee @@ -24,7 +24,7 @@ class App.ChannelChat extends App.Controller '.js-code': 'code' '.js-palette': 'palette' '.js-color': 'colorField' - '.js-chatSetting': 'chatSetting' + '.js-chatSetting input': 'chatSetting' apiOptions: [ { @@ -279,6 +279,8 @@ class App.ChannelChat extends App.Controller setting.state_current = { value: value } setting.save() @Config.set('chat', value) + delay = -> App.Event.trigger('ui:rerender') + @delay(delay, 200) updateParams: => quote = (value) -> diff --git a/app/assets/javascripts/app/controllers/_channel/form.coffee b/app/assets/javascripts/app/controllers/_channel/form.coffee index cc15bff7c..ba587fe0c 100644 --- a/app/assets/javascripts/app/controllers/_channel/form.coffee +++ b/app/assets/javascripts/app/controllers/_channel/form.coffee @@ -7,7 +7,7 @@ class App.ChannelForm extends App.Controller elements: '.js-paramsBlock': 'paramsBlock' - '.js-formSetting': 'formSetting' + '.js-formSetting input': 'formSetting' constructor: -> super diff --git a/app/assets/javascripts/app/controllers/chat.coffee b/app/assets/javascripts/app/controllers/chat.coffee index da34f17da..3815e9825 100644 --- a/app/assets/javascripts/app/controllers/chat.coffee +++ b/app/assets/javascripts/app/controllers/chat.coffee @@ -11,14 +11,6 @@ class App.CustomerChat extends App.Controller constructor: -> super - # access check - if !@isRole('Chat') - @renderScreenUnauthorized(objectName: 'Chat') - return - if !@Config.get('chat') - @renderScreenError(detail: 'Feature disabled!') - return - @chatWindows = {} @maxChatWindows = 4 preferences = @Session.get('preferences') @@ -49,7 +41,6 @@ class App.CustomerChat extends App.Controller # add new chat window @bind('chat_session_start', (data) => - console.log('chat_session_start', data) if data.session @addChat(data.session) ) @@ -78,6 +69,13 @@ class App.CustomerChat extends App.Controller ) render: -> + if !@isRole('Chat') + @renderScreenUnauthorized(objectName: 'Chat') + return + if !@Config.get('chat') + @renderScreenError(detail: 'Feature disabled!') + return + @html App.view('customer_chat/index')() show: (params) => diff --git a/app/assets/javascripts/app/controllers/navigation.coffee b/app/assets/javascripts/app/controllers/navigation.coffee index 27184e1ff..4003f1045 100644 --- a/app/assets/javascripts/app/controllers/navigation.coffee +++ b/app/assets/javascripts/app/controllers/navigation.coffee @@ -82,7 +82,7 @@ class App.Navigation extends App.ControllerWidgetPermanent ) # bind on switch changes and execute it on controller - @$('.js-menu .js-switch').bind('change', (e) -> + @$('.js-menu .js-switch input').bind('change', (e) -> val = $(e.target).prop('checked') key = $(e.target).closest('.menu-item').data('key') return if !key diff --git a/app/assets/javascripts/app/views/channel/chat.jst.eco b/app/assets/javascripts/app/views/channel/chat.jst.eco index c4918513c..865f2fc44 100644 --- a/app/assets/javascripts/app/views/channel/chat.jst.eco +++ b/app/assets/javascripts/app/views/channel/chat.jst.eco @@ -8,8 +8,8 @@

<%- @T('Enable') %>/<%- @T('Disable') %>

-
- checked<% end %>> +
+ checked<% end %>>
diff --git a/app/assets/javascripts/app/views/channel/form.jst.eco b/app/assets/javascripts/app/views/channel/form.jst.eco index b097aea70..decd2070d 100644 --- a/app/assets/javascripts/app/views/channel/form.jst.eco +++ b/app/assets/javascripts/app/views/channel/form.jst.eco @@ -8,8 +8,8 @@

<%- @T('Enable') %>/<%- @T('Disable') %>

-
- checked<% end %>> +
+ checked<% end %>>
diff --git a/app/assets/javascripts/app/views/navigation/menu.jst.eco b/app/assets/javascripts/app/views/navigation/menu.jst.eco index d29a54bda..9d37c8563 100644 --- a/app/assets/javascripts/app/views/navigation/menu.jst.eco +++ b/app/assets/javascripts/app/views/navigation/menu.jst.eco @@ -30,8 +30,8 @@ <%= item.counter %> <% end %> <% if item.switch isnt undefined: %> - - checked<% end %>> + + checked<% end %>> <% end %> diff --git a/app/models/chat/session.rb b/app/models/chat/session.rb index 0edd06435..3742bee69 100644 --- a/app/models/chat/session.rb +++ b/app/models/chat/session.rb @@ -18,6 +18,18 @@ class Chat::Session < ApplicationModel preferences[:participants] end + def recipients_active? + return true if !preferences + return true if !preferences[:participants] + count = 0 + preferences[:participants].each {|client_id| + next if !Sessions.session_exists?(client_id) + count += 1 + } + return true if count >= 2 + false + end + def send_to_recipients(message, ignore_client_id = nil) preferences[:participants].each {|local_client_id| next if local_client_id == ignore_client_id diff --git a/app/models/observer/chat/leave/background_job.rb b/app/models/observer/chat/leave/background_job.rb new file mode 100644 index 000000000..9b897b187 --- /dev/null +++ b/app/models/observer/chat/leave/background_job.rb @@ -0,0 +1,36 @@ +# encoding: utf-8 + +class Observer::Chat::Leave::BackgroundJob + def initialize(chat_session_id, client_id, session) + @chat_session_id = chat_session_id + @client_id = client_id + @session = session + end + + def perform + + # check if customer has permanently left the conversation + chat_session = Chat::Session.find_by(id: @chat_session_id) + return if !chat_session + return if chat_session.recipients_active? + chat_session.state = 'closed' + chat_session.save + + realname = 'Anonymous' + if @session && @session['id'] + realname = User.lookup(id: @session['id']).fullname + end + + # notifiy participients + message = { + event: 'chat_session_left', + data: { + realname: realname, + session_id: chat_session.session_id, + }, + } + chat_session.send_to_recipients(message, @client_id) + + end + +end diff --git a/app/models/setting.rb b/app/models/setting.rb index a74d45c9e..03b6ce0d1 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -16,7 +16,11 @@ class Setting < ApplicationModel @@current = {} # rubocop:disable Style/ClassVars @@change_id = nil # rubocop:disable Style/ClassVars @@lookup_at = nil # rubocop:disable Style/ClassVars - @@lookup_timeout = 2.minutes # rubocop:disable Style/ClassVars + if ENV['ZAMMAD_SETTING_TTL'] + @@lookup_timeout = ENV['ZAMMAD_SETTING_TTL'].to_i # rubocop:disable Style/ClassVars + else + @@lookup_timeout = 2.minutes # rubocop:disable Style/ClassVars + end =begin diff --git a/public/assets/chat/chat.coffee b/public/assets/chat/chat.coffee index 1bb33e972..c0a2582e3 100644 --- a/public/assets/chat/chat.coffee +++ b/public/assets/chat/chat.coffee @@ -5,8 +5,135 @@ do($ = window.jQuery, window) -> scriptHost = myScript.src.match('.*://([^:/]*).*')[1] # Define the plugin class - class ZammadChat + class Base + defaults: + debug: false + constructor: (options) -> + @options = $.extend {}, @defaults, options + @log = new Log(debug: @options.debug, logPrefix: @options.logPrefix || @logPrefix) + + class Log + defaults: + debug: false + + constructor: (options) -> + @options = $.extend {}, @defaults, options + + debug: (items...) => + return if !@options.debug && level is 'debug' + @log('debug', items) + + notice: (items...) => + @log('notice', items) + + error: (items...) => + @log('error', items) + return if !@options.debug && level is 'debug' + items.unshift(level) + console.log.apply console, string + + log: (level, items) => + items.unshift('||') + items.unshift(level) + items.unshift(@options.logPrefix) + console.log.apply console, items + + return if !@options.debug + logString = '' + for item in items + logString += ' ' + if typeof item is 'object' + logString += JSON.stringify(item) + else if item && item.toString + logString += item.toString() + else + logString += item + $('.js-chatLogDisplay').prepend('
' + logString + '
') + + class Timeout extends Base + timeoutStartedAt: null + logPrefix: 'timeout' + defaults: + debug: false + timeout: 4 + timeoutIntervallCheck: 0.5 + + constructor: (options) -> + super(options) + + start: => + @stop() + timeoutStartedAt = new Date + check = => + timeLeft = new Date - new Date(timeoutStartedAt.getTime() + @options.timeout * 1000 * 60) + @log.debug "Timeout check for #{@options.timeout} minutes (left #{timeLeft/1000} sec.)"#, new Date + return if timeLeft < 0 + @stop() + @options.callback() + @log.debug "Start timeout in #{@options.timeout} minutes"#, new Date + @intervallId = setInterval(check, @options.timeoutIntervallCheck * 1000 * 60) + + stop: => + return if !@intervallId + @log.debug "Stop timeout of #{@options.timeout} minutes at"#, new Date + clearInterval(@intervallId) + + class Io extends Base + logPrefix: 'io' + constructor: (options) -> + super(options) + + set: (params) => + for key, value of params + @options[key] = value + + detectHost: -> + protocol = 'ws://' + if window.location.protocol is 'https:' + protocol = 'wss://' + @options.host = "#{ protocol }#{ scriptHost }/ws" + + connect: => + @detectHost() if !@options.host + + @log.debug "Connecting to #{@options.host}" + @ws = new window.WebSocket("#{@options.host}") + @ws.onopen = (e) => + @log.debug 'on open', e + @options.onOpen(e) + + @ws.onmessage = (e) => + pipes = JSON.parse(e.data) + @log.debug 'on message', e.data + if @options.onMessage + @options.onMessage(pipes) + + @ws.onclose = (e) => + @log.debug 'close websocket connection' + if @options.onClose + @options.onClose(e) + + @ws.onerror = (e) => + @log.debug 'onerror', e + if @options.onError + @options.onError(e) + + close: => + @ws.close() + + reconnect: => + @ws.close() + @connect() + + send: (event, data = {}) => + @log.debug 'send', event, data + msg = JSON.stringify + event: event + data: data + @ws.send msg + + class ZammadChat extends Base defaults: chatId: undefined show: true @@ -21,9 +148,14 @@ do($ = window.jQuery, window) -> buttonClass: 'open-zammad-chat' inactiveClass: 'is-inactive' title: 'Chat with us!' - idleTimeout: 4 - inactiveTimeout: 20 + idleTimeout: 8 + idleTimeoutIntervallCheck: 0.5 + inactiveTimeout: 8 + inactiveTimeoutIntervallCheck: 0.5 + waitingListTimeout: 4 + waitingListTimeoutIntervallCheck: 0.5 + logPrefix: 'chat' _messageCount: 0 isOpen: true blinkOnlineInterval: null @@ -35,7 +167,6 @@ do($ = window.jQuery, window) -> isTyping: false state: 'offline' initialQueueDelay: 10000 - wsReconnectEnable: true translations: de: 'Chat with us!': 'Chat mit uns!' @@ -57,22 +188,17 @@ do($ = window.jQuery, window) -> T: (string, items...) => if @options.lang && @options.lang isnt 'en' if !@translations[@options.lang] - @log 'notice', "Translation '#{@options.lang}' needed!" + @log.notice "Translation '#{@options.lang}' needed!" else translations = @translations[@options.lang] if !translations[string] - @log 'notice', "Translation needed for '#{string}'" + @log.notice "Translation needed for '#{string}'" string = translations[string] || string if items for item in items string = string.replace(/%s/, item) string - log: (level, string...) => - return if !@options.debug && level is 'debug' - string.unshift(level) - console.log.apply console, string - view: (name) => return (options) => if !options @@ -86,19 +212,20 @@ do($ = window.jQuery, window) -> constructor: (options) -> @options = $.extend {}, @defaults, options + super(@options) # check prerequisites if !$ @state = 'unsupported' - @log 'notice', 'Chat: no jquery found!' + @log.notice 'Chat: no jquery found!' return if !window.WebSocket or !sessionStorage @state = 'unsupported' - @log 'notice', 'Chat: Browser not supported!' + @log.notice 'Chat: Browser not supported!' return if !@options.chatId @state = 'unsupported' - @log 'error', 'Chat: need chatId as option!' + @log.error 'Chat: need chatId as option!' return # detect language @@ -106,8 +233,19 @@ do($ = window.jQuery, window) -> @options.lang = $('html').attr('lang') if @options.lang @options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx - @log 'debug', "lang: #{@options.lang}" + @log.debug "lang: #{@options.lang}" + @loadCss() + + @io = new Io(@options) + @io.set( + onOpen: @render + onClose: @hide + onMessage: @onWebSocketMessage + ) + @io.connect() + + render: => @el = $(@view('chat')( title: @options.title )) @@ -124,10 +262,20 @@ do($ = window.jQuery, window) -> @input.on keydown: @checkForEnter input: @onInput + $(window).on('beforeunload', => + @onLeaveTemporary() + ) + @setAgentOnlineState 'online' - @wsConnect() + @log.debug 'widget rendered' - @loadCss() + @startTimeoutObservers() + @idleTimeout.start() + + # get current chat status + @sessionId = sessionStorage.getItem('sessionId') + @send 'chat_status_customer', + session_id: @sessionId checkForEnter: (event) => if not event.shiftKey and event.keyCode is 13 @@ -136,22 +284,16 @@ do($ = window.jQuery, window) -> send: (event, data = {}) => data.chat_id = @options.chatId - @log 'debug', 'ws:send', event, data - pipe = JSON.stringify - event: event - data: data - @ws.send pipe - - onWebSocketMessage: (e) => - pipes = JSON.parse( e.data ) + @io.send(event, data) + onWebSocketMessage: (pipes) => for pipe in pipes - @log 'debug', 'ws:onmessage', pipe + @log.debug 'ws:onmessage', pipe switch pipe.event when 'chat_error' - @log 'notice', pipe.data + @log.notice pipe.data if pipe.data && pipe.data.state is 'chat_disabled' - @wsClose() + @destroy(hide: true) when 'chat_session_message' return if pipe.data.self_written @receiveMessage pipe.data @@ -171,38 +313,35 @@ do($ = window.jQuery, window) -> when 'online' @sessionId = undefined @onReady() - @log 'debug', 'Zammad Chat: ready' when 'offline' @onError 'Zammad Chat: No agent online' @state = 'off' - @hide() - @wsClose() + @destroy(hide: true) when 'chat_disabled' @onError 'Zammad Chat: Chat is disabled' @state = 'off' - @hide() - @wsClose() + @destroy(hide: true) when 'no_seats_available' @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}" @state = 'off' - @hide() - @wsClose() + @destroy(hide: true) when 'reconnect' - @log 'debug', 'old messages', pipe.data.session + @log.debug 'old messages', pipe.data.session @reopenSession pipe.data onReady: => + @log.debug 'widget ready for use' $(".#{ @options.buttonClass }").click(@open).removeClass(@inactiveClass) if @options.show @show() onError: (message) => - @log 'debug', message + @log.debug message $(".#{ @options.buttonClass }").hide() reopenSession: (data) => - @inactiveTimeoutStart() + @inactiveTimeout.start() unfinishedMessage = sessionStorage.getItem 'unfinished_message' @@ -246,7 +385,7 @@ do($ = window.jQuery, window) -> @isTyping = new Date() @send 'chat_session_typing', session_id: @sessionId - @inactiveTimeoutStart() + @inactiveTimeout.start() onSubmit: (event) => event.preventDefault() @@ -256,7 +395,7 @@ do($ = window.jQuery, window) -> message = @input.val() return if !message - @inactiveTimeoutStart() + @inactiveTimeout.start() sessionStorage.removeItem 'unfinished_message' @@ -286,7 +425,7 @@ do($ = window.jQuery, window) -> session_id: @sessionId receiveMessage: (data) => - @inactiveTimeoutStart() + @inactiveTimeout.start() # hide writing indicator @onAgentTypingEnd() @@ -305,6 +444,7 @@ do($ = window.jQuery, window) -> @scrollToBottom() open: => + @log.debug 'open widget' if @isOpen @show() @@ -322,12 +462,14 @@ do($ = window.jQuery, window) -> @isOpen = true if !@sessionId - @sessionInit() + @send('chat_session_init') onOpenAnimationEnd: => - @idleTimeoutStop() + @idleTimeout.stop() close: (event) => + @log.debug 'close widget' + return @state if @state is 'off' or @state is 'unsupported' event.stopPropagation() if event @@ -339,7 +481,8 @@ do($ = window.jQuery, window) -> session_id: @sessionId # stop timer - @inactiveTimeoutStop() + @inactiveTimeout.stop() + @waitingListTimeout.stop() # delete input store sessionStorage.removeItem 'unfinished_message' @@ -360,14 +503,20 @@ do($ = window.jQuery, window) -> onCloseAnimationEnd: => @el.removeClass('zammad-chat-is-visible') - @disconnect() + + @showLoader() + @el.find('.zammad-chat-welcome').removeClass('zammad-chat-is-hidden') + @el.find('.zammad-chat-agent').addClass('zammad-chat-is-hidden') + @el.find('.zammad-chat-agent-status').addClass('zammad-chat-is-hidden') + @isOpen = false # restart connection - @onWebSocketOpen() + @io.reconnect() hide: -> - @el.removeClass('zammad-chat-is-shown') + if @el + @el.removeClass('zammad-chat-is-shown') show: -> return @state if @state is 'off' or @state is 'unsupported' @@ -397,6 +546,9 @@ do($ = window.jQuery, window) -> # delay initial queue position, show connecting first show = => @onQueue data + console.log('onQueueScreen') + @waitingListTimeout.start() + if @initialQueueDelay && !@onInitialQueueDelayId @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay) return @@ -409,7 +561,7 @@ do($ = window.jQuery, window) -> show() onQueue: (data) => - @log 'notice', 'onQueue', data.position + @log.notice 'onQueue', data.position @inQueue = true @el.find('.zammad-chat-body').html @view('waiting') @@ -432,6 +584,11 @@ do($ = window.jQuery, window) -> onAgentTypingEnd: => @el.find('.zammad-chat-message--typing').remove() + onLeaveTemporary: => + return if !@sessionId + @send 'chat_session_leave_temporary', + session_id: @sessionId + maybeAddTimestamp: -> timestamp = Date.now() @@ -470,59 +627,38 @@ do($ = window.jQuery, window) -> scrollToBottom: -> @el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight')) - sessionInit: -> - @send('chat_session_init') - detectHost: -> protocol = 'ws://' if window.location.protocol is 'https:' protocol = 'wss://' @options.host = "#{ protocol }#{ scriptHost }/ws" - wsConnect: => - @detectHost() if !@options.host + destroy: (params = {}) => + @log.debug 'destroy widget' + console.log('el', @el) + if params.hide + if @el + @el.remove() + @wsReconnectStop() + @io.close() - @log 'debug', "Connecting to #{@options.host}" - @ws = new window.WebSocket("#{@options.host}") - @ws.onopen = @onWebSocketOpen - - @ws.onmessage = @onWebSocketMessage - - @ws.onclose = (e) => - @log 'debug', 'close websocket connection' - if @wsReconnectEnable - @reconnect() - - @ws.onerror = (e) => - @log 'debug', 'ws:onerror', e - - wsClose: => - @wsReconnectEnable = false - @ws.close() - - wsReconnect: => + wsReconnectStart: => + @wsReconnectStop() if @reconnectDelayId clearTimeout(@reconnectDelayId) - @reconnectDelayId = setTimeout(@wsConnect, 5000) + @reconnectDelayId = setTimeout(@io.connect(), 5000) - onWebSocketOpen: => - @idleTimeoutStart() - @sessionId = sessionStorage.getItem('sessionId') - @log 'debug', 'ws connected' - - @send 'chat_status_customer', - session_id: @sessionId - - @setAgentOnlineState 'online' + wsReconnectStop: => + if @reconnectDelayId + clearTimeout(@reconnectDelayId) reconnect: => # set status to connecting - @log 'notice', 'reconnecting' + @log.notice 'reconnecting' @disableInput() @lastAddedType = 'status' @setAgentOnlineState 'connecting' @addStatus @T('Connection lost') - @wsReconnect() onConnectionReestablished: => # set status back to online @@ -534,13 +670,7 @@ do($ = window.jQuery, window) -> @addStatus @T('Chat closed by %s', data.realname) @disableInput() @setAgentOnlineState 'offline' - @inactiveTimeoutStop() - - disconnect: -> - @showLoader() - @el.find('.zammad-chat-welcome').removeClass('zammad-chat-is-hidden') - @el.find('.zammad-chat-agent').addClass('zammad-chat-is-hidden') - @el.find('.zammad-chat-agent-status').addClass('zammad-chat-is-hidden') + @inactiveTimeout.stop() setSessionId: (id) => @sessionId = id @@ -573,8 +703,12 @@ do($ = window.jQuery, window) -> @setAgentOnlineState 'online' - showTimeout: -> - @el.find('.zammad-chat-body').html @view('timeout') + @waitingListTimeout.stop() + @idleTimeout.stop() + @inactiveTimeout.start() + + showCustomerTimeout: -> + @el.find('.zammad-chat-body').html @view('customer_timeout') agent: @agent.name delay: @options.inactiveTimeout @close() @@ -582,6 +716,14 @@ do($ = window.jQuery, window) -> location.reload() @el.find('.js-restart').click reload + showWaitingListTimeout: -> + @el.find('.zammad-chat-body').html @view('waiting_list_timeout') + delay: @options.watingListTimeout + @close() + reload = -> + location.reload() + @el.find('.js-restart').click reload + showLoader: -> @el.find('.zammad-chat-body').html @view('loader')() @@ -603,42 +745,47 @@ do($ = window.jQuery, window) -> .replace(/\/ws/i, '') url += '/assets/chat/chat.css' - @log 'debug', "load css from '#{url}'" + @log.debug "load css from '#{url}'" styles = "@import url('#{url}');" newSS = document.createElement('link') newSS.rel = 'stylesheet' newSS.href = 'data:text/css,' + escape(styles) document.getElementsByTagName('head')[0].appendChild(newSS) - inactiveTimeoutStart: => - @inactiveTimeoutStop() - delay = => - @log 'debug', "Inactive timeout of #{@options.inactiveTimeout} minutes, show timeout screen.", new Date - @state = 'off' - @setAgentOnlineState 'offline' - @showTimeout() - @wsClose() - @log 'debug', "Start inactive timeout in #{@options.inactiveTimeout} minutes", new Date - @inactiveTimeoutStopDelayId = setTimeout(delay, @options.inactiveTimeout * 1000 * 60) - - inactiveTimeoutStop: => - return if !@inactiveTimeoutStopDelayId - @log 'debug', "Stop inactive timeout of #{@options.inactiveTimeout} minutes at", new Date - clearTimeout(@inactiveTimeoutStopDelayId) - - idleTimeoutStart: => - @idleTimeoutStop() - delay = => - @log 'debug', "Idle timeout of #{@options.idleTimeout} minutes, hide widget", new Date - @state = 'off' - @hide() - @wsClose() - @log 'debug', "Start idle timeout in #{@options.idleTimeout} minutes", new Date - @idleTimeoutStopDelayId = setTimeout(delay, @options.idleTimeout * 1000 * 60) - - idleTimeoutStop: => - return if !@idleTimeoutStopDelayId - @log 'debug', "Stop idle timeout of #{@options.idleTimeout} minutes at", new Date - clearTimeout(@idleTimeoutStopDelayId) + startTimeoutObservers: => + @idleTimeout = new Timeout( + logPrefix: 'idleTimeout' + debug: @options.debug + timeout: @options.idleTimeout + timeoutIntervallCheck: @options.idleTimeoutIntervallCheck + callback: => + @log.debug 'Idle timeout reached, hide widget', new Date + @state = 'off' + @destroy(hide: true) + ) + @inactiveTimeout = new Timeout( + logPrefix: 'inactiveTimeout' + debug: @options.debug + timeout: @options.inactiveTimeout + timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck + callback: => + @log.debug 'Inactive timeout reached, show timeout screen.', new Date + @state = 'off' + @setAgentOnlineState 'offline' + @showCustomerTimeout() + @destroy(hide:false) + ) + @waitingListTimeout = new Timeout( + logPrefix: 'waitingListTimeout' + debug: @options.debug + timeout: @options.waitingListTimeout + timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck + callback: => + @log.debug 'Waiting list timeout reached, show timeout screen.', new Date + @state = 'off' + @setAgentOnlineState 'offline' + @showWaitingListTimeout() + @destroy(hide:false) + ) window.ZammadChat = ZammadChat diff --git a/public/assets/chat/chat.js b/public/assets/chat/chat.js index 3777db598..6201b1ab5 100644 --- a/public/assets/chat/chat.js +++ b/public/assets/chat/chat.js @@ -1,73 +1,246 @@ -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() { - if (this.agent.avatar) { - __out.push('\n\n'); - } - - __out.push('\n\n '); - - __out.push(__sanitize(this.agent.name)); - - __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); }; }, - slice = [].slice; + slice = [].slice, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; (function($, window) { - var ZammadChat, myScript, scriptHost, scripts; + var Base, Io, Log, Timeout, ZammadChat, myScript, scriptHost, scripts; scripts = document.getElementsByTagName('script'); myScript = scripts[scripts.length - 1]; scriptHost = myScript.src.match('.*://([^:/]*).*')[1]; - ZammadChat = (function() { + Base = (function() { + Base.prototype.defaults = { + debug: false + }; + + function Base(options) { + this.options = $.extend({}, this.defaults, options); + this.log = new Log({ + debug: this.options.debug, + logPrefix: this.options.logPrefix || this.logPrefix + }); + } + + return Base; + + })(); + Log = (function() { + Log.prototype.defaults = { + debug: false + }; + + function Log(options) { + this.log = bind(this.log, this); + this.error = bind(this.error, this); + this.notice = bind(this.notice, this); + this.debug = bind(this.debug, this); + this.options = $.extend({}, this.defaults, options); + } + + Log.prototype.debug = function() { + var items; + items = 1 <= arguments.length ? slice.call(arguments, 0) : []; + if (!this.options.debug && level === 'debug') { + return; + } + return this.log('debug', items); + }; + + Log.prototype.notice = function() { + var items; + items = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return this.log('notice', items); + }; + + Log.prototype.error = function() { + var items; + items = 1 <= arguments.length ? slice.call(arguments, 0) : []; + this.log('error', items); + if (!this.options.debug && level === 'debug') { + return; + } + items.unshift(level); + return console.log.apply(console, string); + }; + + Log.prototype.log = function(level, items) { + var i, item, len, logString; + items.unshift('||'); + items.unshift(level); + items.unshift(this.options.logPrefix); + console.log.apply(console, items); + if (!this.options.debug) { + return; + } + logString = ''; + for (i = 0, len = items.length; i < len; i++) { + item = items[i]; + logString += ' '; + if (typeof item === 'object') { + logString += JSON.stringify(item); + } else if (item && item.toString) { + logString += item.toString(); + } else { + logString += item; + } + } + return $('.js-chatLogDisplay').prepend('
' + logString + '
'); + }; + + return Log; + + })(); + Timeout = (function(superClass) { + extend(Timeout, superClass); + + Timeout.prototype.timeoutStartedAt = null; + + Timeout.prototype.logPrefix = 'timeout'; + + Timeout.prototype.defaults = { + debug: false, + timeout: 4, + timeoutIntervallCheck: 0.5 + }; + + function Timeout(options) { + this.stop = bind(this.stop, this); + this.start = bind(this.start, this); + Timeout.__super__.constructor.call(this, options); + } + + Timeout.prototype.start = function() { + var check, timeoutStartedAt; + this.stop(); + timeoutStartedAt = new Date; + check = (function(_this) { + return function() { + var timeLeft; + timeLeft = new Date - new Date(timeoutStartedAt.getTime() + _this.options.timeout * 1000 * 60); + _this.log.debug("Timeout check for " + _this.options.timeout + " minutes (left " + (timeLeft / 1000) + " sec.)"); + if (timeLeft < 0) { + return; + } + _this.stop(); + return _this.options.callback(); + }; + })(this); + this.log.debug("Start timeout in " + this.options.timeout + " minutes"); + return this.intervallId = setInterval(check, this.options.timeoutIntervallCheck * 1000 * 60); + }; + + Timeout.prototype.stop = function() { + if (!this.intervallId) { + return; + } + this.log.debug("Stop timeout of " + this.options.timeout + " minutes at"); + return clearInterval(this.intervallId); + }; + + return Timeout; + + })(Base); + Io = (function(superClass) { + extend(Io, superClass); + + Io.prototype.logPrefix = 'io'; + + function Io(options) { + this.send = bind(this.send, this); + this.reconnect = bind(this.reconnect, this); + this.close = bind(this.close, this); + this.connect = bind(this.connect, this); + this.set = bind(this.set, this); + Io.__super__.constructor.call(this, options); + } + + Io.prototype.set = function(params) { + var key, results, value; + results = []; + for (key in params) { + value = params[key]; + results.push(this.options[key] = value); + } + return results; + }; + + Io.prototype.detectHost = function() { + var protocol; + protocol = 'ws://'; + if (window.location.protocol === 'https:') { + protocol = 'wss://'; + } + return this.options.host = "" + protocol + scriptHost + "/ws"; + }; + + Io.prototype.connect = function() { + if (!this.options.host) { + this.detectHost(); + } + this.log.debug("Connecting to " + this.options.host); + this.ws = new window.WebSocket("" + this.options.host); + this.ws.onopen = (function(_this) { + return function(e) { + _this.log.debug('on open', e); + return _this.options.onOpen(e); + }; + })(this); + this.ws.onmessage = (function(_this) { + return function(e) { + var pipes; + pipes = JSON.parse(e.data); + _this.log.debug('on message', e.data); + if (_this.options.onMessage) { + return _this.options.onMessage(pipes); + } + }; + })(this); + this.ws.onclose = (function(_this) { + return function(e) { + _this.log.debug('close websocket connection'); + if (_this.options.onClose) { + return _this.options.onClose(e); + } + }; + })(this); + return this.ws.onerror = (function(_this) { + return function(e) { + _this.log.debug('onerror', e); + if (_this.options.onError) { + return _this.options.onError(e); + } + }; + })(this); + }; + + Io.prototype.close = function() { + return this.ws.close(); + }; + + Io.prototype.reconnect = function() { + this.ws.close(); + return this.connect(); + }; + + Io.prototype.send = function(event, data) { + var msg; + if (data == null) { + data = {}; + } + this.log.debug('send', event, data); + msg = JSON.stringify({ + event: event, + data: data + }); + return this.ws.send(msg); + }; + + return Io; + + })(Base); + ZammadChat = (function(superClass) { + extend(ZammadChat, superClass); + ZammadChat.prototype.defaults = { chatId: void 0, show: true, @@ -83,9 +256,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); inactiveClass: 'is-inactive', title: 'Chat with us!', idleTimeout: 8, - inactiveTimeout: 20 + idleTimeoutIntervallCheck: 0.5, + inactiveTimeout: 8, + inactiveTimeoutIntervallCheck: 0.5, + waitingListTimeout: 4, + waitingListTimeoutIntervallCheck: 0.5 }; + ZammadChat.prototype.logPrefix = 'chat'; + ZammadChat.prototype._messageCount = 0; ZammadChat.prototype.isOpen = true; @@ -108,8 +287,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); ZammadChat.prototype.initialQueueDelay = 10000; - ZammadChat.prototype.wsReconnectEnable = true; - ZammadChat.prototype.translations = { de: { 'Chat with us!': 'Chat mit uns!', @@ -136,11 +313,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); string = arguments[0], items = 2 <= arguments.length ? slice.call(arguments, 1) : []; if (this.options.lang && this.options.lang !== 'en') { if (!this.translations[this.options.lang]) { - this.log('notice', "Translation '" + this.options.lang + "' needed!"); + this.log.notice("Translation '" + this.options.lang + "' needed!"); } else { translations = this.translations[this.options.lang]; if (!translations[string]) { - this.log('notice', "Translation needed for '" + string + "'"); + this.log.notice("Translation needed for '" + string + "'"); } string = translations[string] || string; } @@ -154,16 +331,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return string; }; - ZammadChat.prototype.log = function() { - var level, string; - level = arguments[0], string = 2 <= arguments.length ? slice.call(arguments, 1) : []; - if (!this.options.debug && level === 'debug') { - return; - } - string.unshift(level); - return console.log.apply(console, string); - }; - ZammadChat.prototype.view = function(name) { return (function(_this) { return function(options) { @@ -180,19 +347,16 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; function ZammadChat(options) { - this.idleTimeoutStop = bind(this.idleTimeoutStop, this); - this.idleTimeoutStart = bind(this.idleTimeoutStart, this); - this.inactiveTimeoutStop = bind(this.inactiveTimeoutStop, this); - this.inactiveTimeoutStart = bind(this.inactiveTimeoutStart, this); + this.startTimeoutObservers = bind(this.startTimeoutObservers, this); this.setAgentOnlineState = bind(this.setAgentOnlineState, this); this.onConnectionEstablished = bind(this.onConnectionEstablished, this); this.setSessionId = bind(this.setSessionId, this); this.onConnectionReestablished = bind(this.onConnectionReestablished, this); this.reconnect = bind(this.reconnect, this); - this.onWebSocketOpen = bind(this.onWebSocketOpen, this); - this.wsReconnect = bind(this.wsReconnect, this); - this.wsClose = bind(this.wsClose, this); - this.wsConnect = bind(this.wsConnect, this); + this.wsReconnectStop = bind(this.wsReconnectStop, this); + this.wsReconnectStart = bind(this.wsReconnectStart, this); + this.destroy = bind(this.destroy, this); + this.onLeaveTemporary = bind(this.onLeaveTemporary, this); this.onAgentTypingEnd = bind(this.onAgentTypingEnd, this); this.onAgentTypingStart = bind(this.onAgentTypingStart, this); this.onQueue = bind(this.onQueue, this); @@ -212,23 +376,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); this.onWebSocketMessage = bind(this.onWebSocketMessage, this); this.send = bind(this.send, this); this.checkForEnter = bind(this.checkForEnter, this); + this.render = bind(this.render, this); this.view = bind(this.view, this); - this.log = bind(this.log, this); this.T = bind(this.T, this); this.options = $.extend({}, this.defaults, options); + ZammadChat.__super__.constructor.call(this, this.options); if (!$) { this.state = 'unsupported'; - this.log('notice', 'Chat: no jquery found!'); + this.log.notice('Chat: no jquery found!'); return; } if (!window.WebSocket || !sessionStorage) { this.state = 'unsupported'; - this.log('notice', 'Chat: Browser not supported!'); + this.log.notice('Chat: Browser not supported!'); return; } if (!this.options.chatId) { this.state = 'unsupported'; - this.log('error', 'Chat: need chatId as option!'); + this.log.error('Chat: need chatId as option!'); return; } if (!this.options.lang) { @@ -236,8 +401,19 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); } if (this.options.lang) { this.options.lang = this.options.lang.replace(/-.+?$/, ''); - this.log('debug', "lang: " + this.options.lang); + this.log.debug("lang: " + this.options.lang); } + this.loadCss(); + this.io = new Io(this.options); + this.io.set({ + onOpen: this.render, + onClose: this.hide, + onMessage: this.onWebSocketMessage + }); + this.io.connect(); + } + + ZammadChat.prototype.render = function() { this.el = $(this.view('chat')({ title: this.options.title })); @@ -251,9 +427,20 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); keydown: this.checkForEnter, input: this.onInput }); - this.wsConnect(); - this.loadCss(); - } + $(window).on('beforeunload', (function(_this) { + return function() { + return _this.onLeaveTemporary(); + }; + })(this)); + this.setAgentOnlineState('online'); + this.log.debug('widget rendered'); + this.startTimeoutObservers(); + this.idleTimeout.start(); + this.sessionId = sessionStorage.getItem('sessionId'); + return this.send('chat_status_customer', { + session_id: this.sessionId + }); + }; ZammadChat.prototype.checkForEnter = function(event) { if (!event.shiftKey && event.keyCode === 13) { @@ -263,30 +450,25 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; ZammadChat.prototype.send = function(event, data) { - var pipe; if (data == null) { data = {}; } data.chat_id = this.options.chatId; - this.log('debug', 'ws:send', event, data); - pipe = JSON.stringify({ - event: event, - data: data - }); - return this.ws.send(pipe); + return this.io.send(event, data); }; - ZammadChat.prototype.onWebSocketMessage = function(e) { - var i, len, pipe, pipes; - pipes = JSON.parse(e.data); + ZammadChat.prototype.onWebSocketMessage = function(pipes) { + var i, len, pipe; for (i = 0, len = pipes.length; i < len; i++) { pipe = pipes[i]; - this.log('debug', 'ws:onmessage', pipe); + this.log.debug('ws:onmessage', pipe); switch (pipe.event) { case 'chat_error': - this.log('notice', pipe.data); + this.log.notice(pipe.data); if (pipe.data && pipe.data.state === 'chat_disabled') { - this.wsClose(); + this.destroy({ + hide: true + }); } break; case 'chat_session_message': @@ -318,28 +500,30 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); case 'online': this.sessionId = void 0; this.onReady(); - this.log('debug', 'Zammad Chat: ready'); break; case 'offline': this.onError('Zammad Chat: No agent online'); this.state = 'off'; - this.hide(); - this.wsClose(); + this.destroy({ + hide: true + }); break; case 'chat_disabled': this.onError('Zammad Chat: Chat is disabled'); this.state = 'off'; - this.hide(); - this.wsClose(); + this.destroy({ + hide: true + }); break; case 'no_seats_available': this.onError("Zammad Chat: Too many clients in queue. Clients in queue: " + pipe.data.queue); this.state = 'off'; - this.hide(); - this.wsClose(); + this.destroy({ + hide: true + }); break; case 'reconnect': - this.log('debug', 'old messages', pipe.data.session); + this.log.debug('old messages', pipe.data.session); this.reopenSession(pipe.data); } } @@ -347,6 +531,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; ZammadChat.prototype.onReady = function() { + this.log.debug('widget ready for use'); $("." + this.options.buttonClass).click(this.open).removeClass(this.inactiveClass); if (this.options.show) { return this.show(); @@ -354,13 +539,13 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; ZammadChat.prototype.onError = function(message) { - this.log('debug', message); + this.log.debug(message); return $("." + this.options.buttonClass).hide(); }; ZammadChat.prototype.reopenSession = function(data) { var i, len, message, ref, unfinishedMessage; - this.inactiveTimeoutStart(); + this.inactiveTimeout.start(); unfinishedMessage = sessionStorage.getItem('unfinished_message'); if (data.agent) { this.onConnectionEstablished(data); @@ -402,7 +587,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); this.send('chat_session_typing', { session_id: this.sessionId }); - return this.inactiveTimeoutStart(); + return this.inactiveTimeout.start(); }; ZammadChat.prototype.onSubmit = function(event) { @@ -416,7 +601,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); if (!message) { return; } - this.inactiveTimeoutStart(); + this.inactiveTimeout.start(); sessionStorage.removeItem('unfinished_message'); messageElement = this.view('message')({ message: message, @@ -442,7 +627,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; ZammadChat.prototype.receiveMessage = function(data) { - this.inactiveTimeoutStart(); + this.inactiveTimeout.start(); this.onAgentTypingEnd(); this.maybeAddTimestamp(); return this.renderMessage({ @@ -460,6 +645,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; ZammadChat.prototype.open = function() { + this.log.debug('open widget'); if (this.isOpen) { this.show(); } @@ -477,15 +663,16 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); } this.isOpen = true; if (!this.sessionId) { - return this.sessionInit(); + return this.send('chat_session_init'); } }; ZammadChat.prototype.onOpenAnimationEnd = function() { - return this.idleTimeoutStop(); + return this.idleTimeout.stop(); }; ZammadChat.prototype.close = function(event) { + this.log.debug('close widget'); if (this.state === 'off' || this.state === 'unsupported') { return this.state; } @@ -498,7 +685,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); this.send('chat_session_close', { session_id: this.sessionId }); - this.inactiveTimeoutStop(); + this.inactiveTimeout.stop(); + this.waitingListTimeout.stop(); sessionStorage.removeItem('unfinished_message'); if (this.onInitialQueueDelayId) { clearTimeout(this.onInitialQueueDelayId); @@ -520,13 +708,18 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); ZammadChat.prototype.onCloseAnimationEnd = function() { this.el.removeClass('zammad-chat-is-visible'); - this.disconnect(); + 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'); this.isOpen = false; - return this.onWebSocketOpen(); + return this.io.reconnect(); }; ZammadChat.prototype.hide = function() { - return this.el.removeClass('zammad-chat-is-shown'); + if (this.el) { + return this.el.removeClass('zammad-chat-is-shown'); + } }; ZammadChat.prototype.show = function() { @@ -560,7 +753,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); this.setSessionId(data.session_id); show = (function(_this) { return function() { - return _this.onQueue(data); + _this.onQueue(data); + console.log('onQueueScreen'); + return _this.waitingListTimeout.start(); }; })(this); if (this.initialQueueDelay && !this.onInitialQueueDelayId) { @@ -574,7 +769,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; ZammadChat.prototype.onQueue = function(data) { - this.log('notice', 'onQueue', data.position); + this.log.notice('onQueue', data.position); this.inQueue = true; return this.el.find('.zammad-chat-body').html(this.view('waiting')({ position: data.position @@ -598,6 +793,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return this.el.find('.zammad-chat-message--typing').remove(); }; + ZammadChat.prototype.onLeaveTemporary = function() { + if (!this.sessionId) { + return; + } + return this.send('chat_session_leave_temporary', { + session_id: this.sessionId + }); + }; + ZammadChat.prototype.maybeAddTimestamp = function() { var label, time, timestamp; timestamp = Date.now(); @@ -638,10 +842,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return this.el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight')); }; - ZammadChat.prototype.sessionInit = function() { - return this.send('chat_session_init'); - }; - ZammadChat.prototype.detectHost = function() { var protocol; protocol = 'ws://'; @@ -651,58 +851,41 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return this.options.host = "" + protocol + scriptHost + "/ws"; }; - ZammadChat.prototype.wsConnect = function() { - if (!this.options.host) { - this.detectHost(); + ZammadChat.prototype.destroy = function(params) { + if (params == null) { + params = {}; } - this.log('debug', "Connecting to " + this.options.host); - this.ws = new window.WebSocket("" + this.options.host); - this.ws.onopen = this.onWebSocketOpen; - this.ws.onmessage = this.onWebSocketMessage; - this.ws.onclose = (function(_this) { - return function(e) { - _this.log('debug', 'close websocket connection'); - if (_this.wsReconnectEnable) { - return _this.reconnect(); - } - }; - })(this); - return this.ws.onerror = (function(_this) { - return function(e) { - return _this.log('debug', 'ws:onerror', e); - }; - })(this); + this.log.debug('destroy widget'); + console.log('el', this.el); + if (params.hide) { + if (this.el) { + this.el.remove(); + } + } + this.wsReconnectStop(); + return this.io.close(); }; - ZammadChat.prototype.wsClose = function() { - this.wsReconnectEnable = false; - return this.ws.close(); - }; - - ZammadChat.prototype.wsReconnect = function() { + ZammadChat.prototype.wsReconnectStart = function() { + this.wsReconnectStop(); if (this.reconnectDelayId) { clearTimeout(this.reconnectDelayId); } - return this.reconnectDelayId = setTimeout(this.wsConnect, 5000); + return this.reconnectDelayId = setTimeout(this.io.connect(), 5000); }; - ZammadChat.prototype.onWebSocketOpen = function() { - this.idleTimeoutStart(); - this.sessionId = sessionStorage.getItem('sessionId'); - this.log('debug', 'ws connected'); - this.send('chat_status_customer', { - session_id: this.sessionId - }); - return this.setAgentOnlineState('online'); + ZammadChat.prototype.wsReconnectStop = function() { + if (this.reconnectDelayId) { + return clearTimeout(this.reconnectDelayId); + } }; ZammadChat.prototype.reconnect = function() { - this.log('notice', 'reconnecting'); + this.log.notice('reconnecting'); this.disableInput(); this.lastAddedType = 'status'; this.setAgentOnlineState('connecting'); - this.addStatus(this.T('Connection lost')); - return this.wsReconnect(); + return this.addStatus(this.T('Connection lost')); }; ZammadChat.prototype.onConnectionReestablished = function() { @@ -715,14 +898,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); this.addStatus(this.T('Chat closed by %s', data.realname)); this.disableInput(); this.setAgentOnlineState('offline'); - return this.inactiveTimeoutStop(); - }; - - ZammadChat.prototype.disconnect = function() { - this.showLoader(); - this.el.find('.zammad-chat-welcome').removeClass('zammad-chat-is-hidden'); - this.el.find('.zammad-chat-agent').addClass('zammad-chat-is-hidden'); - return this.el.find('.zammad-chat-agent-status').addClass('zammad-chat-is-hidden'); + return this.inactiveTimeout.stop(); }; ZammadChat.prototype.setSessionId = function(id) { @@ -754,12 +930,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); this.el.find('.zammad-chat-agent').removeClass('zammad-chat-is-hidden'); this.el.find('.zammad-chat-agent-status').removeClass('zammad-chat-is-hidden'); this.input.focus(); - return this.setAgentOnlineState('online'); + this.setAgentOnlineState('online'); + this.waitingListTimeout.stop(); + this.idleTimeout.stop(); + return this.inactiveTimeout.start(); }; - ZammadChat.prototype.showTimeout = function() { + ZammadChat.prototype.showCustomerTimeout = function() { var reload; - this.el.find('.zammad-chat-body').html(this.view('timeout')({ + this.el.find('.zammad-chat-body').html(this.view('customer_timeout')({ agent: this.agent.name, delay: this.options.inactiveTimeout })); @@ -770,6 +949,18 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return this.el.find('.js-restart').click(reload); }; + ZammadChat.prototype.showWaitingListTimeout = function() { + var reload; + this.el.find('.zammad-chat-body').html(this.view('waiting_list_timeout')({ + delay: this.options.watingListTimeout + })); + this.close(); + reload = function() { + return location.reload(); + }; + return this.el.find('.js-restart').click(reload); + }; + ZammadChat.prototype.showLoader = function() { return this.el.find('.zammad-chat-body').html(this.view('loader')()); }; @@ -791,7 +982,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); url = this.options.host.replace(/^wss/i, 'https').replace(/^ws/i, 'http').replace(/\/ws/i, ''); url += '/assets/chat/chat.css'; } - this.log('debug', "load css from '" + url + "'"); + this.log.debug("load css from '" + url + "'"); styles = "@import url('" + url + "');"; newSS = document.createElement('link'); newSS.rel = 'stylesheet'; @@ -799,52 +990,61 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); return document.getElementsByTagName('head')[0].appendChild(newSS); }; - ZammadChat.prototype.inactiveTimeoutStart = function() { - var delay; - this.inactiveTimeoutStop(); - delay = (function(_this) { - return function() { - _this.log('debug', "Inactive timeout of " + _this.options.inactiveTimeout + " minutes, show timeout screen."); - _this.state = 'off'; - _this.setAgentOnlineState('offline'); - _this.showTimeout(); - return _this.wsClose(); - }; - })(this); - return this.inactiveTimeoutStopDelayId = setTimeout(delay, this.options.inactiveTimeout * 1000 * 60); - }; - - ZammadChat.prototype.inactiveTimeoutStop = function() { - if (!this.inactiveTimeoutStopDelayId) { - return; - } - return clearTimeout(this.inactiveTimeoutStopDelayId); - }; - - ZammadChat.prototype.idleTimeoutStart = function() { - var delay; - this.idleTimeoutStop(); - delay = (function(_this) { - return function() { - _this.log('debug', "Idle timeout of " + _this.options.idleTimeout + " minutes, hide widget"); - _this.state = 'off'; - _this.hide(); - return _this.wsClose(); - }; - })(this); - return this.idleTimeoutStopDelayId = setTimeout(delay, this.options.idleTimeout * 1000 * 60); - }; - - ZammadChat.prototype.idleTimeoutStop = function() { - if (!this.idleTimeoutStopDelayId) { - return; - } - return clearTimeout(this.idleTimeoutStopDelayId); + ZammadChat.prototype.startTimeoutObservers = function() { + this.idleTimeout = new Timeout({ + logPrefix: 'idleTimeout', + debug: this.options.debug, + timeout: this.options.idleTimeout, + timeoutIntervallCheck: this.options.idleTimeoutIntervallCheck, + callback: (function(_this) { + return function() { + _this.log.debug('Idle timeout reached, hide widget', new Date); + _this.state = 'off'; + return _this.destroy({ + hide: true + }); + }; + })(this) + }); + this.inactiveTimeout = new Timeout({ + logPrefix: 'inactiveTimeout', + debug: this.options.debug, + timeout: this.options.inactiveTimeout, + timeoutIntervallCheck: this.options.inactiveTimeoutIntervallCheck, + callback: (function(_this) { + return function() { + _this.log.debug('Inactive timeout reached, show timeout screen.', new Date); + _this.state = 'off'; + _this.setAgentOnlineState('offline'); + _this.showCustomerTimeout(); + return _this.destroy({ + hide: false + }); + }; + })(this) + }); + return this.waitingListTimeout = new Timeout({ + logPrefix: 'waitingListTimeout', + debug: this.options.debug, + timeout: this.options.waitingListTimeout, + timeoutIntervallCheck: this.options.waitingListTimeoutIntervallCheck, + callback: (function(_this) { + return function() { + _this.log.debug('Waiting list timeout reached, show timeout screen.', new Date); + _this.state = 'off'; + _this.setAgentOnlineState('offline'); + _this.showWaitingListTimeout(); + return _this.destroy({ + hide: false + }); + }; + })(this) + }); }; return ZammadChat; - })(); + })(Base); return window.ZammadChat = ZammadChat; })(window.jQuery, window); @@ -933,6 +1133,67 @@ jQuery.fn.autoGrow = function(options) { }); }; +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() { + if (this.agent.avatar) { + __out.push('\n\n'); + } + + __out.push('\n\n '); + + __out.push(__sanitize(this.agent.name)); + + __out.push('\n'); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + if (!window.zammadChatTemplates) { window.zammadChatTemplates = {}; } @@ -1020,6 +1281,79 @@ window.zammadChatTemplates["chat"] = function (__obj) { return __out.join(''); }; +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["customer_timeout"] = 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 '); + + if (this.agent) { + __out.push('\n '); + __out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation with %s got closed.', this.delay, this.agent)); + __out.push('\n '); + } else { + __out.push('\n '); + __out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation got closed.', this.delay)); + __out.push('\n '); + } + + __out.push('\n
\n
'); + + __out.push(this.T('Start new conversation')); + + __out.push('
\n
\n
'); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; + if (!window.zammadChatTemplates) { window.zammadChatTemplates = {}; } @@ -1197,79 +1531,6 @@ window.zammadChatTemplates["status"] = function (__obj) { return __out.join(''); }; -if (!window.zammadChatTemplates) { - window.zammadChatTemplates = {}; -} -window.zammadChatTemplates["timeout"] = 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 '); - - if (this.agent) { - __out.push('\n '); - __out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation with %s got closed.', this.delay, this.agent)); - __out.push('\n '); - } else { - __out.push('\n '); - __out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation got closed.', this.delay)); - __out.push('\n '); - } - - __out.push('\n
\n
'); - - __out.push(this.T('Start new conversation')); - - __out.push('
\n
\n
'); - - }).call(this); - - }).call(__obj); - __obj.safe = __objSafe, __obj.escape = __escape; - return __out.join(''); -}; - if (!window.zammadChatTemplates) { window.zammadChatTemplates = {}; } @@ -1438,3 +1699,68 @@ window.zammadChatTemplates["waiting"] = function (__obj) { __obj.safe = __objSafe, __obj.escape = __escape; return __out.join(''); }; + +if (!window.zammadChatTemplates) { + window.zammadChatTemplates = {}; +} +window.zammadChatTemplates["waiting_list_timeout"] = 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(this.T('We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!')); + + __out.push('\n
\n
'); + + __out.push(this.T('Start new conversation')); + + __out.push('
\n
\n
'); + + }).call(this); + + }).call(__obj); + __obj.safe = __objSafe, __obj.escape = __escape; + return __out.join(''); +}; diff --git a/public/assets/chat/chat.min.js b/public/assets/chat/chat.min.js index 2662e6a84..41aa391d3 100644 --- a/public/assets/chat/chat.min.js +++ b/public/assets/chat/chat.min.js @@ -1 +1,2 @@ -window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?a(t):""},i=t.safe,a=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},a||(a=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(s.push('\n\n')),s.push('\n\n '),s.push(n(this.agent.name)),s.push("\n")}).call(this)}.call(t),t.safe=i,t.escape=a,s.join("")};var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice;!function(t,e){var s,n,i,a;return a=document.getElementsByTagName("script"),n=a[a.length-1],i=n.src.match(".*://([^:/]*).*")[1],s=function(){function s(s){return this.idleTimeoutStop=bind(this.idleTimeoutStop,this),this.idleTimeoutStart=bind(this.idleTimeoutStart,this),this.inactiveTimeoutStop=bind(this.inactiveTimeoutStop,this),this.inactiveTimeoutStart=bind(this.inactiveTimeoutStart,this),this.setAgentOnlineState=bind(this.setAgentOnlineState,this),this.onConnectionEstablished=bind(this.onConnectionEstablished,this),this.setSessionId=bind(this.setSessionId,this),this.onConnectionReestablished=bind(this.onConnectionReestablished,this),this.reconnect=bind(this.reconnect,this),this.onWebSocketOpen=bind(this.onWebSocketOpen,this),this.wsReconnect=bind(this.wsReconnect,this),this.wsClose=bind(this.wsClose,this),this.wsConnect=bind(this.wsConnect,this),this.onAgentTypingEnd=bind(this.onAgentTypingEnd,this),this.onAgentTypingStart=bind(this.onAgentTypingStart,this),this.onQueue=bind(this.onQueue,this),this.onQueueScreen=bind(this.onQueueScreen,this),this.onCloseAnimationEnd=bind(this.onCloseAnimationEnd,this),this.closeWindow=bind(this.closeWindow,this),this.close=bind(this.close,this),this.onOpenAnimationEnd=bind(this.onOpenAnimationEnd,this),this.open=bind(this.open,this),this.renderMessage=bind(this.renderMessage,this),this.receiveMessage=bind(this.receiveMessage,this),this.onSubmit=bind(this.onSubmit,this),this.onInput=bind(this.onInput,this),this.reopenSession=bind(this.reopenSession,this),this.onError=bind(this.onError,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.log=bind(this.log,this),this.T=bind(this.T,this),this.options=t.extend({},this.defaults,s),t?e.WebSocket&&sessionStorage?this.options.chatId?(this.options.lang||(this.options.lang=t("html").attr("lang")),this.options.lang&&(this.options.lang=this.options.lang.replace(/-.+?$/,""),this.log("debug","lang: "+this.options.lang)),this.el=t(this.view("chat")({title:this.options.title})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-close").click(this.close),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.input.on({keydown:this.checkForEnter,input:this.onInput}),this.wsConnect(),void this.loadCss()):(this.state="unsupported",void this.log("error","Chat: need chatId as option!")):(this.state="unsupported",void this.log("notice","Chat: Browser not supported!")):(this.state="unsupported",void this.log("notice","Chat: no jquery found!"))}return s.prototype.defaults={chatId:void 0,show:!0,target:t("body"),host:"",debug:!1,flat:!1,lang:void 0,cssAutoload:!0,cssUrl:void 0,fontSize:void 0,buttonClass:"open-zammad-chat",inactiveClass:"is-inactive",title:"Chat with us!",idleTimeout:8,inactiveTimeout:20},s.prototype._messageCount=0,s.prototype.isOpen=!0,s.prototype.blinkOnlineInterval=null,s.prototype.stopBlinOnlineStateTimeout=null,s.prototype.showTimeEveryXMinutes=1,s.prototype.lastTimestamp=null,s.prototype.lastAddedType=null,s.prototype.inputTimeout=null,s.prototype.isTyping=!1,s.prototype.state="offline",s.prototype.initialQueueDelay=1e4,s.prototype.wsReconnectEnable=!0,s.prototype.translations={de:{"Chat with us!":"Chat mit uns!",Online:"Online",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Compose your message...":"Ihre Nachricht...","All colleges are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen."}},s.prototype.sessionId=void 0,s.prototype.T=function(){var t,e,s,n,i,a;if(i=arguments[0],s=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?(a=this.translations[this.options.lang],a[i]||this.log("notice","Translation needed for '"+i+"'"),i=a[i]||i):this.log("notice","Translation '"+this.options.lang+"' needed!")),s)for(t=0,n=s.length;n>t;t++)e=s[t],i=i.replace(/%s/,e);return i},s.prototype.log=function(){var t,e;return t=arguments[0],e=2<=arguments.length?slice.call(arguments,1):[],this.options.debug||"debug"!==t?(e.unshift(t),console.log.apply(console,e)):void 0},s.prototype.view=function(t){return function(s){return function(n){return n||(n={}),n.T=s.T,n.background=s.options.background,n.flat=s.options.flat,n.fontSize=s.options.fontSize,e.zammadChatTemplates[t](n)}}(this)},s.prototype.checkForEnter=function(t){return t.shiftKey||13!==t.keyCode?void 0:(t.preventDefault(),this.sendMessage())},s.prototype.send=function(t,e){var s;return null==e&&(e={}),e.chat_id=this.options.chatId,this.log("debug","ws:send",t,e),s=JSON.stringify({event:t,data:e}),this.ws.send(s)},s.prototype.onWebSocketMessage=function(t){var e,s,n,i;for(i=JSON.parse(t.data),e=0,s=i.length;s>e;e++)switch(n=i[e],this.log("debug","ws:onmessage",n),n.event){case"chat_error":this.log("notice",n.data),n.data&&"chat_disabled"===n.data.state&&this.wsClose();break;case"chat_session_message":if(n.data.self_written)return;this.receiveMessage(n.data);break;case"chat_session_typing":if(n.data.self_written)return;this.onAgentTypingStart();break;case"chat_session_start":this.onConnectionEstablished(n.data);break;case"chat_session_queue":this.onQueueScreen(n.data);break;case"chat_session_closed":this.onSessionClosed(n.data);break;case"chat_session_left":this.onSessionClosed(n.data);break;case"chat_status_customer":switch(n.data.state){case"online":this.sessionId=void 0,this.onReady(),this.log("debug","Zammad Chat: ready");break;case"offline":this.onError("Zammad Chat: No agent online"),this.state="off",this.hide(),this.wsClose();break;case"chat_disabled":this.onError("Zammad Chat: Chat is disabled"),this.state="off",this.hide(),this.wsClose();break;case"no_seats_available":this.onError("Zammad Chat: Too many clients in queue. Clients in queue: "+n.data.queue),this.state="off",this.hide(),this.wsClose();break;case"reconnect":this.log("debug","old messages",n.data.session),this.reopenSession(n.data)}}},s.prototype.onReady=function(){return t("."+this.options.buttonClass).click(this.open).removeClass(this.inactiveClass),this.options.show?this.show():void 0},s.prototype.onError=function(e){return this.log("debug",e),t("."+this.options.buttonClass).hide()},s.prototype.reopenSession=function(t){var e,s,n,i,a;if(this.inactiveTimeoutStart(),a=sessionStorage.getItem("unfinished_message"),t.agent){for(this.onConnectionEstablished(t),i=t.session,e=0,s=i.length;s>e;e++)n=i[e],this.renderMessage({message:n.content,id:n.id,from:n.created_by_id?"agent":"customer"});a&&this.input.val(a)}return t.position&&this.onQueue(t),this.show(),this.open(),this.scrollToBottom(),a?this.input.focus():void 0},s.prototype.onInput=function(){return this.el.find(".zammad-chat-message--unread").removeClass("zammad-chat-message--unread"),sessionStorage.setItem("unfinished_message",this.input.val()),this.onTyping()},s.prototype.onTyping=function(){return this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)?void 0:(this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeoutStart())},s.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},s.prototype.sendMessage=function(){var t,e;return(t=this.input.val())?(this.inactiveTimeoutStart(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),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.input.val(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})):void 0},s.prototype.receiveMessage=function(t){return this.inactiveTimeoutStart(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"})},s.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t)),this.scrollToBottom()},s.prototype.open=function(){return this.isOpen&&this.show(),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.isOpen=!0,this.sessionId?void 0:this.sessionInit()},s.prototype.onOpenAnimationEnd=function(){return this.idleTimeoutStop()},s.prototype.close=function(t){return"off"===this.state||"unsupported"===this.state?this.state:(t&&t.stopPropagation(),this.sessionId?(this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeoutStop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),t&&this.closeWindow(),this.setSessionId(void 0)):void 0)},s.prototype.closeWindow=function(){var t;return this.el.removeClass("zammad-chat-is-open"),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-t},500,this.onCloseAnimationEnd)},s.prototype.onCloseAnimationEnd=function(){return this.el.removeClass("zammad-chat-is-visible"),this.disconnect(),this.isOpen=!1,this.onWebSocketOpen()},s.prototype.hide=function(){return this.el.removeClass("zammad-chat-is-shown")},s.prototype.show=function(){var t;return"off"===this.state||"unsupported"===this.state?this.state:(this.el.addClass("zammad-chat-is-shown"),this.inputInitialized||(this.inputInitialized=!0,this.input.autoGrow({extraLine:!1})),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t))},s.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},s.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1)},s.prototype.onQueueScreen=function(t){var e;return this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t)}}(this),this.initialQueueDelay&&!this.onInitialQueueDelayId?void(this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)):(this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e())},s.prototype.onQueue=function(t){return this.log("notice","onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-body").html(this.view("waiting")({position:t.position}))},s.prototype.onAgentTypingStart=function(){return this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),this.el.find(".zammad-chat-message--typing").size()?void 0:(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.scrollToBottom())},s.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},s.prototype.maybeAddTimestamp=function(){var t,e,s;return s=Date.now(),!this.lastTimestamp||s-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=s):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=s,this.lastAddedType="timestamp",this.scrollToBottom())):void 0},s.prototype.updateLastTimestamp=function(t,e){return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e}))},s.prototype.addStatus=function(t){return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()},s.prototype.scrollToBottom=function(){return this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight"))},s.prototype.sessionInit=function(){return this.send("chat_session_init")},s.prototype.detectHost=function(){var t;return t="ws://","https:"===e.location.protocol&&(t="wss://"),this.options.host=""+t+i+"/ws"},s.prototype.wsConnect=function(){return this.options.host||this.detectHost(),this.log("debug","Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=this.onWebSocketOpen,this.ws.onmessage=this.onWebSocketMessage,this.ws.onclose=function(t){return function(e){return t.log("debug","close websocket connection"),t.wsReconnectEnable?t.reconnect():void 0}}(this),this.ws.onerror=function(t){return function(e){return t.log("debug","ws:onerror",e)}}(this)},s.prototype.wsClose=function(){return this.wsReconnectEnable=!1,this.ws.close()},s.prototype.wsReconnect=function(){return this.reconnectDelayId&&clearTimeout(this.reconnectDelayId),this.reconnectDelayId=setTimeout(this.wsConnect,5e3)},s.prototype.onWebSocketOpen=function(){return this.idleTimeoutStart(),this.sessionId=sessionStorage.getItem("sessionId"),this.log("debug","ws connected"),this.send("chat_status_customer",{session_id:this.sessionId}),this.setAgentOnlineState("online")},s.prototype.reconnect=function(){return this.log("notice","reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost")),this.wsReconnect()},s.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},s.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeoutStop()},s.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")},s.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},s.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),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.input.focus(),this.setAgentOnlineState("online")},s.prototype.showTimeout=function(){var t;return this.el.find(".zammad-chat-body").html(this.view("timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),this.close(),t=function(){return location.reload()},this.el.find(".js-restart").click(t)},s.prototype.showLoader=function(){return this.el.find(".zammad-chat-body").html(this.view("loader")())},s.prototype.setAgentOnlineState=function(t){var e;return this.state=t,e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))},s.prototype.loadCss=function(){var t,e,s;if(this.options.cssAutoload)return s=this.options.cssUrl,s||(s=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),s+="/assets/chat/chat.css"),this.log("debug","load css from '"+s+"'"),e="@import url('"+s+"');",t=document.createElement("link"),t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},s.prototype.inactiveTimeoutStart=function(){var t;return this.inactiveTimeoutStop(),t=function(t){return function(){return t.log("debug","Inactive timeout of "+t.options.inactiveTimeout+" minutes, show timeout screen."),t.state="off",t.setAgentOnlineState("offline"),t.showTimeout(),t.wsClose()}}(this),this.inactiveTimeoutStopDelayId=setTimeout(t,1e3*this.options.inactiveTimeout*60)},s.prototype.inactiveTimeoutStop=function(){return this.inactiveTimeoutStopDelayId?clearTimeout(this.inactiveTimeoutStopDelayId):void 0},s.prototype.idleTimeoutStart=function(){var t;return this.idleTimeoutStop(),t=function(t){return function(){return t.log("debug","Idle timeout of "+t.options.idleTimeout+" minutes, hide widget"),t.state="off",t.hide(),t.wsClose()}}(this),this.idleTimeoutStopDelayId=setTimeout(t,1e3*this.options.idleTimeout*60)},s.prototype.idleTimeoutStop=function(){return this.idleTimeoutStopDelayId?clearTimeout(this.idleTimeoutStopDelayId):void 0},s}(),e.ZammadChat=s}(window.jQuery,window),jQuery.fn.autoGrow=function(t){return this.each(function(){var e=jQuery.extend({extraLine:!0},t),s=function(t){return jQuery(t).after('
'),jQuery(t).next(".autogrow-textarea-mirror")[0]},n=function(t){if(a.innerHTML=String(t.value).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">").replace(/ /g," ").replace(/\n/g,"
")+(e.extraLine?".
.":""),jQuery(t).height()!=jQuery(a).height()){jQuery(t).height(jQuery(a).height());var s=parseInt(jQuery(t).css("max-height"),10),n=jQuery(a).height()>s?"":"hidden";jQuery(t).css("overflow",n)}},i=function(){n(this)},a=s(this);a.style.display="none",a.style.wordWrap="break-word",a.style.whiteSpace="normal",a.style.padding=jQuery(this).css("paddingTop")+" "+jQuery(this).css("paddingRight")+" "+jQuery(this).css("paddingBottom")+" "+jQuery(this).css("paddingLeft"),a.style.width=jQuery(this).css("width"),a.style.fontFamily=jQuery(this).css("font-family"),a.style.fontSize=jQuery(this).css("font-size"),a.style.lineHeight=jQuery(this).css("line-height"),this.style.overflow="hidden",this.style.minHeight=this.rows+"em",this.onkeyup=i,n(this)})},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.chat=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?a(t):""},i=t.safe,a=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},a||(a=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n
\n
\n \n \n \n \n \n
\n
\n
\n
\n \n '),s.push(this.T(this.title)),s.push('\n
\n
\n
\n
\n \n \n
\n
")}).call(this)}.call(t),t.safe=i,t.escape=a,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e,s=[],n=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(){s.push('
\n \n \n \n \n \n '),s.push(this.T("Connecting")),s.push("\n
")}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?a(t):""},i=t.safe,a=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},a||(a=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n "),s.push(this.message),s.push("\n
")}).call(this)}.call(t),t.safe=i,t.escape=a,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e,s=[],n=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(){s.push('
'),s.push(this.status),s.push("
")}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timeout=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?a(t):""},i=t.safe,a=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},a||(a=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n
\n '),this.agent?(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),s.push("\n ")):(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),s.push("\n ")),s.push('\n
\n
"),s.push(this.T("Start new conversation")),s.push("
\n
\n
")}).call(this)}.call(t),t.safe=i,t.escape=a,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?a(t):""},i=t.safe,a=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},a||(a=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
'),s.push(n(this.label)),s.push(" "),s.push(n(this.time)),s.push("
")}).call(this)}.call(t),t.safe=i,t.escape=a,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e,s=[],n=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(){s.push('
\n \n \n \n \n \n \n \n
')}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e,s=[],n=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(){s.push('
\n
\n \n \n \n \n \n '),s.push(this.T("All colleges are busy.")),s.push("
\n "),s.push(this.T("You are on waiting list position %s.",this.position)),s.push("\n
\n
")}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")}; \ No newline at end of file +var bind=function(t,e){return function(){return t.apply(e,arguments)}},slice=[].slice,extend=function(t,e){function s(){this.constructor=t}for(var n in e)hasProp.call(e,n)&&(t[n]=e[n]);return s.prototype=e.prototype,t.prototype=new s,t.__super__=e.prototype,t},hasProp={}.hasOwnProperty;!function(t,e){var s,n,i,o,a,r,h,d;return d=document.getElementsByTagName("script"),r=d[d.length-1],h=r.src.match(".*://([^:/]*).*")[1],s=function(){function e(e){this.options=t.extend({},this.defaults,e),this.log=new i({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),i=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=t.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.options.debug||"debug"!==level?this.log("debug",t):void 0},e.prototype.notice=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",t)},e.prototype.error=function(){var t;return t=1<=arguments.length?slice.call(arguments,0):[],this.log("error",t),this.options.debug||"debug"!==level?(t.unshift(level),console.log.apply(console,string)):void 0},e.prototype.log=function(e,s){var n,i,o,a;if(s.unshift("||"),s.unshift(e),s.unshift(this.options.logPrefix),console.log.apply(console,s),this.options.debug){for(a="",n=0,o=s.length;o>n;n++)i=s[n],a+=" ",a+="object"==typeof i?JSON.stringify(i):i&&i.toString?i.toString():i;return t(".js-chatLogDisplay").prepend("
"+a+"
")}},e}(),o=function(t){function e(t){this.stop=bind(this.stop,this),this.start=bind(this.start,this),e.__super__.constructor.call(this,t)}return extend(e,t),e.prototype.timeoutStartedAt=null,e.prototype.logPrefix="timeout",e.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},e.prototype.start=function(){var t,e;return this.stop(),e=new Date,t=function(t){return function(){var s;return s=new Date-new Date(e.getTime()+1e3*t.options.timeout*60),t.log.debug("Timeout check for "+t.options.timeout+" minutes (left "+s/1e3+" sec.)"),0>s?void 0:(t.stop(),t.options.callback())}}(this),this.log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(t,1e3*this.options.timeoutIntervallCheck*60)},e.prototype.stop=function(){return this.intervallId?(this.log.debug("Stop timeout of "+this.options.timeout+" minutes at"),clearInterval(this.intervallId)):void 0},e}(s),n=function(t){function s(t){this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),s.__super__.constructor.call(this,t)}return extend(s,t),s.prototype.logPrefix="io",s.prototype.set=function(t){var e,s,n;s=[];for(e in t)n=t[e],s.push(this.options[e]=n);return s},s.prototype.detectHost=function(){var t;return t="ws://","https:"===e.location.protocol&&(t="wss://"),this.options.host=""+t+h+"/ws"},s.prototype.connect=function(){return this.options.host||this.detectHost(),this.log.debug("Connecting to "+this.options.host),this.ws=new e.WebSocket(""+this.options.host),this.ws.onopen=function(t){return function(e){return t.log.debug("on open",e),t.options.onOpen(e)}}(this),this.ws.onmessage=function(t){return function(e){var s;return s=JSON.parse(e.data),t.log.debug("on message",e.data),t.options.onMessage?t.options.onMessage(s):void 0}}(this),this.ws.onclose=function(t){return function(e){return t.log.debug("close websocket connection"),t.options.onClose?t.options.onClose(e):void 0}}(this),this.ws.onerror=function(t){return function(e){return t.log.debug("onerror",e),t.options.onError?t.options.onError(e):void 0}}(this)},s.prototype.close=function(){return this.ws.close()},s.prototype.reconnect=function(){return this.ws.close(),this.connect()},s.prototype.send=function(t,e){var s;return null==e&&(e={}),this.log.debug("send",t,e),s=JSON.stringify({event:t,data:e}),this.ws.send(s)},s}(s),a=function(s){function i(s){return this.startTimeoutObservers=bind(this.startTimeoutObservers,this),this.setAgentOnlineState=bind(this.setAgentOnlineState,this),this.onConnectionEstablished=bind(this.onConnectionEstablished,this),this.setSessionId=bind(this.setSessionId,this),this.onConnectionReestablished=bind(this.onConnectionReestablished,this),this.reconnect=bind(this.reconnect,this),this.wsReconnectStop=bind(this.wsReconnectStop,this),this.wsReconnectStart=bind(this.wsReconnectStart,this),this.destroy=bind(this.destroy,this),this.onLeaveTemporary=bind(this.onLeaveTemporary,this),this.onAgentTypingEnd=bind(this.onAgentTypingEnd,this),this.onAgentTypingStart=bind(this.onAgentTypingStart,this),this.onQueue=bind(this.onQueue,this),this.onQueueScreen=bind(this.onQueueScreen,this),this.onCloseAnimationEnd=bind(this.onCloseAnimationEnd,this),this.closeWindow=bind(this.closeWindow,this),this.close=bind(this.close,this),this.onOpenAnimationEnd=bind(this.onOpenAnimationEnd,this),this.open=bind(this.open,this),this.renderMessage=bind(this.renderMessage,this),this.receiveMessage=bind(this.receiveMessage,this),this.onSubmit=bind(this.onSubmit,this),this.onInput=bind(this.onInput,this),this.reopenSession=bind(this.reopenSession,this),this.onError=bind(this.onError,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.render=bind(this.render,this),this.view=bind(this.view,this),this.T=bind(this.T,this),this.options=t.extend({},this.defaults,s),i.__super__.constructor.call(this,this.options),t?e.WebSocket&&sessionStorage?this.options.chatId?(this.options.lang||(this.options.lang=t("html").attr("lang")),this.options.lang&&(this.options.lang=this.options.lang.replace(/-.+?$/,""),this.log.debug("lang: "+this.options.lang)),this.loadCss(),this.io=new n(this.options),this.io.set({onOpen:this.render,onClose:this.hide,onMessage:this.onWebSocketMessage}),void this.io.connect()):(this.state="unsupported",void this.log.error("Chat: need chatId as option!")):(this.state="unsupported",void this.log.notice("Chat: Browser not supported!")):(this.state="unsupported",void this.log.notice("Chat: no jquery found!"))}return extend(i,s),i.prototype.defaults={chatId:void 0,show:!0,target:t("body"),host:"",debug:!1,flat:!1,lang:void 0,cssAutoload:!0,cssUrl:void 0,fontSize:void 0,buttonClass:"open-zammad-chat",inactiveClass:"is-inactive",title:"Chat with us!",idleTimeout:8,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5},i.prototype.logPrefix="chat",i.prototype._messageCount=0,i.prototype.isOpen=!0,i.prototype.blinkOnlineInterval=null,i.prototype.stopBlinOnlineStateTimeout=null,i.prototype.showTimeEveryXMinutes=1,i.prototype.lastTimestamp=null,i.prototype.lastAddedType=null,i.prototype.inputTimeout=null,i.prototype.isTyping=!1,i.prototype.state="offline",i.prototype.initialQueueDelay=1e4,i.prototype.translations={de:{"Chat with us!":"Chat mit uns!",Online:"Online",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Compose your message...":"Ihre Nachricht...","All colleges are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen."}},i.prototype.sessionId=void 0,i.prototype.T=function(){var t,e,s,n,i,o;if(i=arguments[0],s=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?(o=this.translations[this.options.lang],o[i]||this.log.notice("Translation needed for '"+i+"'"),i=o[i]||i):this.log.notice("Translation '"+this.options.lang+"' needed!")),s)for(t=0,n=s.length;n>t;t++)e=s[t],i=i.replace(/%s/,e);return i},i.prototype.view=function(t){return function(s){return function(n){return n||(n={}),n.T=s.T,n.background=s.options.background,n.flat=s.options.flat,n.fontSize=s.options.fontSize,e.zammadChatTemplates[t](n)}}(this)},i.prototype.render=function(){return this.el=t(this.view("chat")({title:this.options.title})),this.options.target.append(this.el),this.input=this.el.find(".zammad-chat-input"),t("."+this.options.buttonClass).addClass(this.inactiveClass),this.el.find(".js-chat-open").click(this.open),this.el.find(".js-chat-close").click(this.close),this.el.find(".zammad-chat-controls").on("submit",this.onSubmit),this.input.on({keydown:this.checkForEnter,input:this.onInput}),t(e).on("beforeunload",function(t){return function(){return t.onLeaveTemporary()}}(this)),this.setAgentOnlineState("online"),this.log.debug("widget rendered"),this.startTimeoutObservers(),this.idleTimeout.start(),this.sessionId=sessionStorage.getItem("sessionId"),this.send("chat_status_customer",{session_id:this.sessionId})},i.prototype.checkForEnter=function(t){return t.shiftKey||13!==t.keyCode?void 0:(t.preventDefault(),this.sendMessage())},i.prototype.send=function(t,e){return null==e&&(e={}),e.chat_id=this.options.chatId,this.io.send(t,e)},i.prototype.onWebSocketMessage=function(t){var e,s,n;for(e=0,s=t.length;s>e;e++)switch(n=t[e],this.log.debug("ws:onmessage",n),n.event){case"chat_error":this.log.notice(n.data),n.data&&"chat_disabled"===n.data.state&&this.destroy({hide:!0});break;case"chat_session_message":if(n.data.self_written)return;this.receiveMessage(n.data);break;case"chat_session_typing":if(n.data.self_written)return;this.onAgentTypingStart();break;case"chat_session_start":this.onConnectionEstablished(n.data);break;case"chat_session_queue":this.onQueueScreen(n.data);break;case"chat_session_closed":this.onSessionClosed(n.data);break;case"chat_session_left":this.onSessionClosed(n.data);break;case"chat_status_customer":switch(n.data.state){case"online":this.sessionId=void 0,this.onReady();break;case"offline":this.onError("Zammad Chat: No agent online"),this.state="off",this.destroy({hide:!0});break;case"chat_disabled":this.onError("Zammad Chat: Chat is disabled"),this.state="off",this.destroy({hide:!0});break;case"no_seats_available":this.onError("Zammad Chat: Too many clients in queue. Clients in queue: "+n.data.queue),this.state="off",this.destroy({hide:!0});break;case"reconnect":this.log.debug("old messages",n.data.session),this.reopenSession(n.data)}}},i.prototype.onReady=function(){return this.log.debug("widget ready for use"),t("."+this.options.buttonClass).click(this.open).removeClass(this.inactiveClass),this.options.show?this.show():void 0},i.prototype.onError=function(e){return this.log.debug(e),t("."+this.options.buttonClass).hide()},i.prototype.reopenSession=function(t){var e,s,n,i,o;if(this.inactiveTimeout.start(),o=sessionStorage.getItem("unfinished_message"),t.agent){for(this.onConnectionEstablished(t),i=t.session,e=0,s=i.length;s>e;e++)n=i[e],this.renderMessage({message:n.content,id:n.id,from:n.created_by_id?"agent":"customer"});o&&this.input.val(o)}return t.position&&this.onQueue(t),this.show(),this.open(),this.scrollToBottom(),o?this.input.focus():void 0},i.prototype.onInput=function(){return this.el.find(".zammad-chat-message--unread").removeClass("zammad-chat-message--unread"),sessionStorage.setItem("unfinished_message",this.input.val()),this.onTyping()},i.prototype.onTyping=function(){return this.isTyping&&this.isTyping>new Date((new Date).getTime()-1500)?void 0:(this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start())},i.prototype.onSubmit=function(t){return t.preventDefault(),this.sendMessage()},i.prototype.sendMessage=function(){var t,e;return(t=this.input.val())?(this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),e=this.view("message")({message:t,from:"customer",id:this._messageCount++,unreadClass:""}),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.input.val(""),this.scrollToBottom(),this.send("chat_session_message",{content:t,id:this._messageCount,session_id:this.sessionId})):void 0},i.prototype.receiveMessage=function(t){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:t.message.content,id:t.id,from:"agent"})},i.prototype.renderMessage=function(t){return this.lastAddedType="message--"+t.from,t.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(t)),this.scrollToBottom()},i.prototype.open=function(){return this.log.debug("open widget"),this.isOpen&&this.show(),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.isOpen=!0,this.sessionId?void 0:this.send("chat_session_init")},i.prototype.onOpenAnimationEnd=function(){return this.idleTimeout.stop()},i.prototype.close=function(t){return this.log.debug("close widget"),"off"===this.state||"unsupported"===this.state?this.state:(t&&t.stopPropagation(),this.sessionId?(this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),t&&this.closeWindow(),this.setSessionId(void 0)):void 0)},i.prototype.closeWindow=function(){var t;return this.el.removeClass("zammad-chat-is-open"),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-t},500,this.onCloseAnimationEnd)},i.prototype.onCloseAnimationEnd=function(){return this.el.removeClass("zammad-chat-is-visible"),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"),this.isOpen=!1,this.io.reconnect()},i.prototype.hide=function(){return this.el?this.el.removeClass("zammad-chat-is-shown"):void 0},i.prototype.show=function(){var t;return"off"===this.state||"unsupported"===this.state?this.state:(this.el.addClass("zammad-chat-is-shown"),this.inputInitialized||(this.inputInitialized=!0,this.input.autoGrow({extraLine:!1})),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-t))},i.prototype.disableInput=function(){return this.input.prop("disabled",!0),this.el.find(".zammad-chat-send").prop("disabled",!0)},i.prototype.enableInput=function(){return this.input.prop("disabled",!1),this.el.find(".zammad-chat-send").prop("disabled",!1)},i.prototype.onQueueScreen=function(t){var e;return this.setSessionId(t.session_id),e=function(e){return function(){return e.onQueue(t),console.log("onQueueScreen"),e.waitingListTimeout.start()}}(this),this.initialQueueDelay&&!this.onInitialQueueDelayId?void(this.onInitialQueueDelayId=setTimeout(e,this.initialQueueDelay)):(this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),e())},i.prototype.onQueue=function(t){return this.log.notice("onQueue",t.position),this.inQueue=!0,this.el.find(".zammad-chat-body").html(this.view("waiting")({position:t.position}))},i.prototype.onAgentTypingStart=function(){return this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),this.el.find(".zammad-chat-message--typing").size()?void 0:(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.scrollToBottom())},i.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},i.prototype.onLeaveTemporary=function(){return this.sessionId?this.send("chat_session_leave_temporary",{session_id:this.sessionId}):void 0},i.prototype.maybeAddTimestamp=function(){var t,e,s;return s=Date.now(),!this.lastTimestamp||s-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=s):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:t,time:e})),this.lastTimestamp=s,this.lastAddedType="timestamp",this.scrollToBottom())):void 0},i.prototype.updateLastTimestamp=function(t,e){return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:t,time:e}))},i.prototype.addStatus=function(t){return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:t})),this.scrollToBottom()},i.prototype.scrollToBottom=function(){return this.el.find(".zammad-chat-body").scrollTop(t(".zammad-chat-body").prop("scrollHeight"))},i.prototype.detectHost=function(){var t;return t="ws://","https:"===e.location.protocol&&(t="wss://"),this.options.host=""+t+h+"/ws"},i.prototype.destroy=function(t){return null==t&&(t={}),this.log.debug("destroy widget"),console.log("el",this.el),t.hide&&this.el&&this.el.remove(),this.wsReconnectStop(),this.io.close()},i.prototype.wsReconnectStart=function(){return this.wsReconnectStop(),this.reconnectDelayId&&clearTimeout(this.reconnectDelayId),this.reconnectDelayId=setTimeout(this.io.connect(),5e3)},i.prototype.wsReconnectStop=function(){return this.reconnectDelayId?clearTimeout(this.reconnectDelayId):void 0},i.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},i.prototype.onConnectionReestablished=function(){return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established"))},i.prototype.onSessionClosed=function(t){return this.addStatus(this.T("Chat closed by %s",t.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop()},i.prototype.setSessionId=function(t){return this.sessionId=t,void 0===t?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",t)},i.prototype.onConnectionEstablished=function(t){return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,t.agent&&(this.agent=t.agent),t.session_id&&this.setSessionId(t.session_id),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),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.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start()},i.prototype.showCustomerTimeout=function(){var t;return this.el.find(".zammad-chat-body").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),this.close(),t=function(){return location.reload()},this.el.find(".js-restart").click(t)},i.prototype.showWaitingListTimeout=function(){var t;return this.el.find(".zammad-chat-body").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),this.close(),t=function(){return location.reload()},this.el.find(".js-restart").click(t)},i.prototype.showLoader=function(){return this.el.find(".zammad-chat-body").html(this.view("loader")())},i.prototype.setAgentOnlineState=function(t){var e;return this.state=t,e=t.charAt(0).toUpperCase()+t.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",t).text(this.T(e))},i.prototype.loadCss=function(){var t,e,s;if(this.options.cssAutoload)return s=this.options.cssUrl,s||(s=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws/i,""),s+="/assets/chat/chat.css"),this.log.debug("load css from '"+s+"'"),e="@import url('"+s+"');",t=document.createElement("link"),t.rel="stylesheet",t.href="data:text/css,"+escape(e),document.getElementsByTagName("head")[0].appendChild(t)},i.prototype.startTimeoutObservers=function(){return this.idleTimeout=new o({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Idle timeout reached, hide widget",new Date),t.state="off",t.destroy({hide:!0})}}(this)}),this.inactiveTimeout=new o({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.state="off",t.setAgentOnlineState("offline"),t.showCustomerTimeout(),t.destroy({hide:!1})}}(this)}),this.waitingListTimeout=new o({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:function(t){return function(){return t.log.debug("Waiting list timeout reached, show timeout screen.",new Date),t.state="off",t.setAgentOnlineState("offline"),t.showWaitingListTimeout(),t.destroy({hide:!1})}}(this)})},i}(s),e.ZammadChat=a}(window.jQuery,window),jQuery.fn.autoGrow=function(t){return this.each(function(){var e=jQuery.extend({extraLine:!0},t),s=function(t){return jQuery(t).after('
'),jQuery(t).next(".autogrow-textarea-mirror")[0]},n=function(t){if(o.innerHTML=String(t.value).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">").replace(/ /g," ").replace(/\n/g,"
")+(e.extraLine?".
.":""),jQuery(t).height()!=jQuery(o).height()){jQuery(t).height(jQuery(o).height());var s=parseInt(jQuery(t).css("max-height"),10),n=jQuery(o).height()>s?"":"hidden";jQuery(t).css("overflow",n)}},i=function(){n(this)},o=s(this);o.style.display="none",o.style.wordWrap="break-word",o.style.whiteSpace="normal",o.style.padding=jQuery(this).css("paddingTop")+" "+jQuery(this).css("paddingRight")+" "+jQuery(this).css("paddingBottom")+" "+jQuery(this).css("paddingLeft"),o.style.width=jQuery(this).css("width"),o.style.fontFamily=jQuery(this).css("font-family"),o.style.fontSize=jQuery(this).css("font-size"),o.style.lineHeight=jQuery(this).css("line-height"),this.style.overflow="hidden",this.style.minHeight=this.rows+"em",this.onkeyup=i,n(this)})},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=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},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(s.push('\n\n')),s.push('\n\n '),s.push(n(this.agent.name)),s.push("\n")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.chat=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=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},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n
\n
\n \n \n \n \n \n
\n
\n
\n
\n \n '),s.push(this.T(this.title)),s.push('\n
\n
\n
\n
\n \n \n
\n
")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=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},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n
\n '),this.agent?(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent)),s.push("\n ")):(s.push("\n "),s.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay)),s.push("\n ")),s.push('\n
\n
"),s.push(this.T("Start new conversation")),s.push("
\n
\n
")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(t){t||(t={});var e,s=[],n=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(){s.push('
\n \n \n \n \n \n '),s.push(this.T("Connecting")),s.push("\n
")}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=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},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n "),s.push(this.message),s.push("\n
")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(t){t||(t={});var e,s=[],n=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(){s.push('
'),s.push(this.status),s.push("
")}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=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},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
'),s.push(n(this.label)),s.push(" "),s.push(n(this.time)),s.push("
")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(t){t||(t={});var e,s=[],n=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(){s.push('
\n \n \n \n \n \n \n \n
')}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(t){t||(t={});var e,s=[],n=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(){s.push('
\n
\n \n \n \n \n \n '),s.push(this.T("All colleges are busy.")),s.push("
\n "),s.push(this.T("You are on waiting list position %s.",this.position)),s.push("\n
\n
")}).call(this)}.call(t),t.safe=n,t.escape=i,s.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(t){t||(t={});var e,s=[],n=function(t){return t&&t.ecoSafe?t:"undefined"!=typeof t&&null!=t?o(t):""},i=t.safe,o=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},o||(o=t.escape=function(t){return(""+t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){s.push('
\n
\n '),s.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),s.push('\n
\n
"),s.push(this.T("Start new conversation")),s.push("
\n
\n
")}).call(this)}.call(t),t.safe=i,t.escape=o,s.join("")}; \ No newline at end of file diff --git a/public/assets/chat/views/timeout.eco b/public/assets/chat/views/customer_timeout.eco similarity index 100% rename from public/assets/chat/views/timeout.eco rename to public/assets/chat/views/customer_timeout.eco diff --git a/public/assets/chat/views/waiting_list_timeout.eco b/public/assets/chat/views/waiting_list_timeout.eco new file mode 100644 index 000000000..9370bfcb0 --- /dev/null +++ b/public/assets/chat/views/waiting_list_timeout.eco @@ -0,0 +1,7 @@ +
+
+ <%- @T('We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!') %> +
+
><%- @T('Start new conversation') %>
+
+
\ No newline at end of file diff --git a/public/assets/chat/znuny.html b/public/assets/chat/znuny.html index a4c92bcf1..79d43434f 100644 --- a/public/assets/chat/znuny.html +++ b/public/assets/chat/znuny.html @@ -25,6 +25,7 @@ padding: 10px; border-radius: 5px; box-shadow: 0 3px 10px rgba(0,0,0,.3); + width: 500px; } .settings input { @@ -39,6 +40,12 @@ padding-right: 0; } + table td.log { + text-align: left; + padding-right: 0; + word-break: break-all; + } + td { padding: 5px; } @@ -73,8 +80,8 @@
- + +

Settings

+
@@ -100,6 +107,11 @@
+

Log

+
+
@@ -113,7 +125,13 @@ debug: true, background: '#494d52', flat: true, - shown: false + shown: false, + idleTimeout: 1, + idleTimeoutIntervallCheck: 0.5, + inactiveTimeout: 2, + inactiveTimeoutIntervallCheck: 0.5, + waitingListTimeout: 1.2, + waitingListTimeoutIntervallCheck: 0.5, }); $('.settings :input').on({ diff --git a/script/websocket-server.rb b/script/websocket-server.rb index 6b7a0181a..fc29de9b8 100755 --- a/script/websocket-server.rb +++ b/script/websocket-server.rb @@ -257,6 +257,7 @@ EventMachine.run { } elsif data['event'] + log 'notice', "execute event '#{data['event']}'", client_id message = Sessions::Event.run(data['event'], data, @clients[client_id][:session], client_id) if message websocket_send(client_id, message) diff --git a/test/browser/chat_test.rb b/test/browser/chat_test.rb index 1d94eb657..4409b4bbd 100644 --- a/test/browser/chat_test.rb +++ b/test/browser/chat_test.rb @@ -1,128 +1,428 @@ # encoding: utf-8 -# rubocop:disable all require 'browser_test_helper' class ChatTest < TestCase - def test_websocket - return # TODO: temp disable - message = 'message 1äöüß ' + rand(99_999_999_999_999_999).to_s - tests = [ - { - name: 'start', - instance1: browser_instance, - instance2: browser_instance, - instance1_username: 'master@example.com', - instance1_password: 'test', - instance2_username: 'agent1@example.com', - instance2_password: 'test', - url: browser_url, - action: [ - { - where: :instance1, - execute: 'check', - css: '#login', - result: false, - }, - { - where: :instance2, - execute: 'check', - css: '#login', - result: false, - }, - { - execute: 'wait', - value: 1, - }, - { - where: :instance1, - execute: 'click', - css: '#chat_toogle', - }, - { - execute: 'wait', - value: 8, - }, - { - where: :instance1, - execute: 'click', - css: '#chat_toogle', - }, - { - execute: 'wait', - value: 4, - }, - { - where: :instance2, - execute: 'click', - css: '#chat_toogle', - }, - { - where: :instance1, - execute: 'click', - css: '#chat_toogle', - }, - { - execute: 'wait', - value: 2, - }, - { - where: :instance1, - execute: 'set', - css: 'input[name="chat_message"]', - value: message, - }, - { - where: :instance1, - execute: 'send_key', - css: 'input[name="chat_message"]', - value: :enter, - }, - { - execute: 'wait', - value: 6, - }, - { - where: :instance1, - execute: 'match', - css: '#chat_log_container', - value: message, - match_result: true, - }, - { - where: :instance2, - execute: 'match', - css: '#chat_log_container', - value: message, - match_result: true, - }, - { - where: :instance1, - execute: 'navigate', - to: browser_url, - }, - { - execute: 'wait', - value: 8, - }, - { - where: :instance1, - execute: 'click', - css: '#chat_toogle', - }, - { - execute: 'wait', - value: 8, - }, - { - where: :instance1, - execute: 'match', - css: '#chat_log_container', - value: message, - match_result: true, - }, - ], - }, - ] - browser_double_test(tests) + + def test_basic + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + + # disable chat + click( + browser: agent, + css: 'a[href="#manage"]', + ) + click( + browser: agent, + css: 'a[href="#channels/chat"]', + ) + switch( + browser: agent, + css: '#content .js-chatSetting', + type: 'off', + ) + sleep 25 # wait for rerendering + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + match( + browser: agent, + css: '.active.content', + value: 'disabled', + ) + + customer = browser_instance + location( + browser: customer, + url: "#{browser_url}/assets/chat/znuny.html", + ) + sleep 4 + exists_not( + browser: customer, + css: '.zammad-chat', + ) + match( + browser: customer, + css: '.settings', + value: '{"state":"chat_disabled"}', + ) + click( + browser: agent, + css: 'a[href="#manage"]', + ) + click( + browser: agent, + css: 'a[href="#channels/chat"]', + ) + switch( + browser: agent, + css: '#content .js-chatSetting', + type: 'on', + ) + sleep 15 # wait for rerendering + switch( + browser: agent, + css: '#navigation .js-switch', + type: 'off', + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + wait: 2, + ) + match_not( + browser: agent, + css: '.active.content', + value: 'disabled', + ) + + reload( + browser: customer, + ) + sleep 4 + exists_not( + browser: customer, + css: '.zammad-chat', + ) + match_not( + browser: customer, + css: '.settings', + value: '{"state":"chat_disabled"}', + ) + match( + browser: customer, + css: '.settings', + value: '{"event":"chat_status_customer","data":{"state":"offline"}}', + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + switch( + browser: agent, + css: '#navigation .js-switch', + type: 'on', + ) + reload( + browser: customer, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + match_not( + browser: customer, + css: '.settings', + value: '{"state":"chat_disabled"}', + ) + match_not( + browser: customer, + css: '.settings', + value: '{"event":"chat_status_customer","data":{"state":"offline"}}', + ) + match( + browser: customer, + css: '.settings', + value: '"data":{"state":"online"}', + ) + + # init chat + click( + browser: customer, + css: '.js-chat-open', + ) + exists( + browser: customer, + css: '.zammad-chat-is-shown', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + watch_for( + browser: agent, + css: '.js-chatMenuItem .counter', + value: '1', + ) + click( + browser: customer, + css: '.js-chat-close', + ) + watch_for_disappear( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + watch_for_disappear( + browser: agent, + css: '.js-chatMenuItem .counter', + ) + end + + def test_basic_usecase1 + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click) + + customer = browser_instance + location( + browser: customer, + url: "#{browser_url}/assets/chat/znuny.html", + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + click( + browser: customer, + css: '.js-chat-open', + ) + exists( + browser: customer, + css: '.zammad-chat-is-shown', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + + click( + browser: agent, + css: '.active .js-acceptChat', + ) + sleep 2 + exists_not( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + set( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + value: 'my name is me', + ) + click( + browser: agent, + css: '.active .chat-window .js-send', + ) + watch_for( + browser: customer, + css: '.zammad-chat .zammad-chat-agent-status', + value: 'online', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'my name is me', + ) + set( + browser: customer, + css: '.zammad-chat .zammad-chat-input', + value: 'my name is customer', + ) + click( + browser: customer, + css: '.zammad-chat .zammad-chat-send', + ) + watch_for( + browser: agent, + css: '.active .chat-window', + value: 'my name is customer', + ) + exists( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + click( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + ) + exists_not( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + click( + browser: customer, + css: '.js-chat-close', + ) + watch_for( + browser: agent, + css: '.active .chat-window', + value: 'has left the conversation', + ) + end + + def test_basic_usecase2 + agent = browser_instance + login( + browser: agent, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all( + browser: agent, + ) + click( + browser: agent, + css: 'a[href="#customer_chat"]', + ) + agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click) + + customer = browser_instance + location( + browser: customer, + url: "#{browser_url}/assets/chat/znuny.html", + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + click( + browser: customer, + css: '.js-chat-open', + ) + exists( + browser: customer, + css: '.zammad-chat-is-shown', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + ) + click( + browser: agent, + css: '.active .js-acceptChat', + ) + sleep 2 + exists_not( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + watch_for( + browser: customer, + css: '.zammad-chat .zammad-chat-agent-status', + value: 'online', + ) + set( + browser: customer, + css: '.zammad-chat .zammad-chat-input', + value: 'my name is customer', + ) + click( + browser: customer, + css: '.zammad-chat .zammad-chat-send', + ) + watch_for( + browser: agent, + css: '.active .chat-window', + value: 'my name is customer', + ) + exists( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + set( + browser: agent, + css: '.active .chat-window .js-customerChatInput', + value: 'my name is me', + ) + exists_not( + browser: agent, + css: '.active .chat-window .chat-status.is-modified', + ) + click( + browser: agent, + css: '.active .chat-window .js-send', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'my name is me', + ) + click( + browser: agent, + css: '.active .chat-window .js-close', + ) + watch_for( + browser: customer, + css: '.zammad-chat .zammad-chat-agent-status', + value: 'offline', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: 'Chat closed by', + ) + end + + def test_timeouts + customer = browser_instance + location( + browser: customer, + url: "#{browser_url}/assets/chat/znuny.html", + ) + watch_for( + browser: customer, + css: '.zammad-chat', + timeout: 5, + ) + watch_for_disappear( + browser: customer, + css: '.zammad-chat', + timeout: 75, + ) + reload( + browser: customer, + ) + exists( + browser: customer, + css: '.zammad-chat', + ) + click( + browser: customer, + css: '.js-chat-open', + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(waiting|warte)', + timeout: 35, + ) + watch_for( + browser: customer, + css: '.zammad-chat', + value: '(takes longer|dauert länger)', + timeout: 90, + ) + + end + end