Fixed timing issues, improved tests.

This commit is contained in:
Martin Edenhofer 2015-12-08 12:32:19 +01:00
parent 465ab3e6be
commit f06348e843
7 changed files with 409 additions and 274 deletions

View file

@ -291,7 +291,7 @@ class ChatWindow extends App.Controller
phrasesArray = phrases.split(';') phrasesArray = phrases.split(';')
phrase = phrasesArray[_.random(0, phrasesArray.length-1)] phrase = phrasesArray[_.random(0, phrasesArray.length-1)]
@input.html(phrase) @input.html(phrase)
@sendMessage() @sendMessage(1600)
focus: => focus: =>
@input.focus() @input.focus()
@ -372,16 +372,27 @@ class ChatWindow extends App.Controller
event.preventDefault() event.preventDefault()
@sendMessage() @sendMessage()
sendMessage: => sendMessage: (delay) =>
content = @input.html() content = @input.html()
return if !content return if !content
App.WebSocket.send( send = =>
event:'chat_session_message' App.WebSocket.send(
data: event:'chat_session_message'
content: content data:
session_id: @session.session_id content: content
) session_id: @session.session_id
)
if !delay
send()
else
# show key enter and send phrase
App.WebSocket.send(
event:'chat_session_typing'
data:
session_id: @session.session_id
)
@delay(send, delay)
@addMessage content, 'agent' @addMessage content, 'agent'
@input.html('') @input.html('')

View file

@ -73,7 +73,7 @@ do($ = window.jQuery, window) ->
stop: => stop: =>
return if !@intervallId return if !@intervallId
@log.debug "Stop timeout of #{@options.timeout} minutes at"#, new Date @log.debug "Stop timeout of #{@options.timeout} minutes"#, new Date
clearInterval(@intervallId) clearInterval(@intervallId)
class Io extends Base class Io extends Base
@ -89,30 +89,40 @@ do($ = window.jQuery, window) ->
@log.debug "Connecting to #{@options.host}" @log.debug "Connecting to #{@options.host}"
@ws = new window.WebSocket("#{@options.host}") @ws = new window.WebSocket("#{@options.host}")
@ws.onopen = (e) => @ws.onopen = (e) =>
@log.debug 'on open', e @log.debug 'onOpen', e
@options.onOpen(e) @options.onOpen(e)
@ws.onmessage = (e) => @ws.onmessage = (e) =>
pipes = JSON.parse(e.data) pipes = JSON.parse(e.data)
@log.debug 'on message', e.data @log.debug 'onMessage', e.data
if @options.onMessage if @options.onMessage
@options.onMessage(pipes) @options.onMessage(pipes)
@ws.onclose = (e) => @ws.onclose = (e) =>
@log.debug 'close websocket connection' @log.debug 'close websocket connection', e
if @options.onClose if @manualClose
@options.onClose(e) @log.debug 'manual close, onClose callback'
@manualClose = false
if @options.onClose
@options.onClose(e)
else
@log.debug 'error close, onError callback'
if @options.onError
@options.onError('Connection lost...')
@ws.onerror = (e) => @ws.onerror = (e) =>
@log.debug 'onerror', e @log.debug 'onError', e
if @options.onError if @options.onError
@options.onError(e) @options.onError(e)
close: => close: =>
@log.debug 'close websocket manually'
@manualClose = true
@ws.close() @ws.close()
reconnect: => reconnect: =>
@ws.close() @log.debug 'reconnect'
@close()
@connect() @connect()
send: (event, data = {}) => send: (event, data = {}) =>
@ -137,7 +147,7 @@ do($ = window.jQuery, window) ->
buttonClass: 'open-zammad-chat' buttonClass: 'open-zammad-chat'
inactiveClass: 'is-inactive' inactiveClass: 'is-inactive'
title: '<strong>Chat</strong> with us!' title: '<strong>Chat</strong> with us!'
idleTimeout: 8 idleTimeout: 6
idleTimeoutIntervallCheck: 0.5 idleTimeoutIntervallCheck: 0.5
inactiveTimeout: 8 inactiveTimeout: 8
inactiveTimeoutIntervallCheck: 0.5 inactiveTimeoutIntervallCheck: 0.5
@ -146,7 +156,7 @@ do($ = window.jQuery, window) ->
logPrefix: 'chat' logPrefix: 'chat'
_messageCount: 0 _messageCount: 0
isOpen: true isOpen: false
blinkOnlineInterval: null blinkOnlineInterval: null
stopBlinOnlineStateTimeout: null stopBlinOnlineStateTimeout: null
showTimeEveryXMinutes: 1 showTimeEveryXMinutes: 1
@ -232,31 +242,39 @@ do($ = window.jQuery, window) ->
@io = new Io(@options) @io = new Io(@options)
@io.set( @io.set(
onOpen: @render onOpen: @render
onClose: @hide onClose: @onWebSocketClose
onMessage: @onWebSocketMessage onMessage: @onWebSocketMessage
onError: @onError
) )
@io.connect() @io.connect()
render: => render: =>
@el = $(@view('chat')( if !@el || !$('.zammad-chat').get(0)
title: @options.title @el = $(@view('chat')(
)) title: @options.title
@options.target.append @el ))
@options.target.append @el
@input = @el.find('.zammad-chat-input') @input = @el.find('.zammad-chat-input')
# start bindings
@el.find('.js-chat-open').click @open
@el.find('.js-chat-close').click @close
@el.find('.zammad-chat-controls').on 'submit', @onSubmit
@input.on
keydown: @checkForEnter
input: @onInput
$(window).on('beforeunload', =>
@onLeaveTemporary()
)
$(window).bind('hashchange', =>
return if @isOpen
@idleTimeout.start()
)
# disable open button # disable open button
$(".#{ @options.buttonClass }").addClass @inactiveClass $(".#{ @options.buttonClass }").addClass @inactiveClass
@el.find('.js-chat-open').click @open
@el.find('.js-chat-close').click @close
@el.find('.zammad-chat-controls').on 'submit', @onSubmit
@input.on
keydown: @checkForEnter
input: @onInput
$(window).on('beforeunload', =>
@onLeaveTemporary()
)
@setAgentOnlineState 'online' @setAgentOnlineState 'online'
@log.debug 'widget rendered' @log.debug 'widget rendered'
@ -285,7 +303,7 @@ do($ = window.jQuery, window) ->
when 'chat_error' when 'chat_error'
@log.notice pipe.data @log.notice pipe.data
if pipe.data && pipe.data.state is 'chat_disabled' if pipe.data && pipe.data.state is 'chat_disabled'
@destroy(hide: true) @destroy(remove: true)
when 'chat_session_message' when 'chat_session_message'
return if pipe.data.self_written return if pipe.data.self_written
@receiveMessage pipe.data @receiveMessage pipe.data
@ -307,21 +325,14 @@ do($ = window.jQuery, window) ->
@onReady() @onReady()
when 'offline' when 'offline'
@onError 'Zammad Chat: No agent online' @onError 'Zammad Chat: No agent online'
@state = 'off'
@destroy(hide: true)
when 'chat_disabled' when 'chat_disabled'
@onError 'Zammad Chat: Chat is disabled' @onError 'Zammad Chat: Chat is disabled'
@state = 'off'
@destroy(hide: true)
when 'no_seats_available' when 'no_seats_available'
@onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}" @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}"
@state = 'off'
@destroy(hide: true)
when 'reconnect' when 'reconnect'
@log.debug 'old messages', pipe.data.session @onReopenSession pipe.data
@reopenSession pipe.data
onReady: => onReady: ->
@log.debug 'widget ready for use' @log.debug 'widget ready for use'
$(".#{ @options.buttonClass }").click(@open).removeClass(@inactiveClass) $(".#{ @options.buttonClass }").click(@open).removeClass(@inactiveClass)
@ -330,9 +341,16 @@ do($ = window.jQuery, window) ->
onError: (message) => onError: (message) =>
@log.debug message @log.debug message
@addStatus(message)
$(".#{ @options.buttonClass }").hide() $(".#{ @options.buttonClass }").hide()
if @isOpen
@disableInput()
@destroy(remove: false)
else
@destroy(remove: true)
reopenSession: (data) => onReopenSession: (data) =>
@log.debug 'old messages', data.session
@inactiveTimeout.start() @inactiveTimeout.start()
unfinishedMessage = sessionStorage.getItem 'unfinished_message' unfinishedMessage = sessionStorage.getItem 'unfinished_message'
@ -436,9 +454,12 @@ do($ = window.jQuery, window) ->
@scrollToBottom() @scrollToBottom()
open: => open: =>
@log.debug 'open widget'
if @isOpen if @isOpen
@show() @log.debug 'widget already open, block'
return
@isOpen = true
@log.debug 'open widget'
if !@sessionId if !@sessionId
@showLoader() @showLoader()
@ -447,27 +468,15 @@ do($ = window.jQuery, window) ->
if !@sessionId if !@sessionId
@el.animate { bottom: 0 }, 500, @onOpenAnimationEnd @el.animate { bottom: 0 }, 500, @onOpenAnimationEnd
@send('chat_session_init')
else else
@el.css 'bottom', 0 @el.css 'bottom', 0
@onOpenAnimationEnd() @onOpenAnimationEnd()
@isOpen = true
if !@sessionId
@send('chat_session_init')
onOpenAnimationEnd: => onOpenAnimationEnd: =>
@idleTimeout.stop() @idleTimeout.stop()
close: (event) => sessionClose: =>
@log.debug 'close widget'
return @state if @state is 'off' or @state is 'unsupported'
event.stopPropagation() if event
# only close if session_id exists
return if !@sessionId
# send close # send close
@send 'chat_session_close', @send 'chat_session_close',
session_id: @sessionId session_id: @sessionId
@ -483,12 +492,23 @@ do($ = window.jQuery, window) ->
if @onInitialQueueDelayId if @onInitialQueueDelayId
clearTimeout(@onInitialQueueDelayId) clearTimeout(@onInitialQueueDelayId)
if event
@closeWindow()
@setSessionId undefined @setSessionId undefined
closeWindow: => close: (event) =>
if !@isOpen
@log.debug 'can\'t close widget, it\'s not open'
return
if !@sessionId
@log.debug 'can\'t close widget without sessionId'
return
@log.debug 'close widget'
event.stopPropagation() if event
@sessionClose()
# close window
@el.removeClass('zammad-chat-is-open') @el.removeClass('zammad-chat-is-open')
remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight() remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
@el.animate { bottom: -remainerHeight }, 500, @onCloseAnimationEnd @el.animate { bottom: -remainerHeight }, 500, @onCloseAnimationEnd
@ -503,15 +523,15 @@ do($ = window.jQuery, window) ->
@isOpen = false @isOpen = false
# restart connection
@io.reconnect() @io.reconnect()
hide: -> onWebSocketClose: =>
return if @isOpen
if @el if @el
@el.removeClass('zammad-chat-is-shown') @el.removeClass('zammad-chat-is-shown')
show: -> show: ->
return @state if @state is 'off' or @state is 'unsupported' return if @state is 'offline'
@el.addClass('zammad-chat-is-shown') @el.addClass('zammad-chat-is-shown')
@ -600,6 +620,7 @@ do($ = window.jQuery, window) ->
@scrollToBottom() @scrollToBottom()
updateLastTimestamp: (label, time) -> updateLastTimestamp: (label, time) ->
return if !@el
@el.find('.zammad-chat-body') @el.find('.zammad-chat-body')
.find('.zammad-chat-timestamp') .find('.zammad-chat-timestamp')
.last() .last()
@ -608,6 +629,7 @@ do($ = window.jQuery, window) ->
time: time time: time
addStatus: (status) -> addStatus: (status) ->
return if !@el
@maybeAddTimestamp() @maybeAddTimestamp()
@el.find('.zammad-chat-body').append @view('status') @el.find('.zammad-chat-body').append @view('status')
@ -619,30 +641,24 @@ do($ = window.jQuery, window) ->
@el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight')) @el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'))
destroy: (params = {}) => destroy: (params = {}) =>
@log.debug 'destroy widget' @log.debug 'destroy widget', params
if params.hide
if @el @setAgentOnlineState 'offline'
@el.remove()
if params.remove && @el
@el.remove()
# stop all timer # stop all timer
@waitingListTimeout.stop() if @waitingListTimeout
@inactiveTimeout.stop() @waitingListTimeout.stop()
@idleTimeout.stop() if @inactiveTimeout
@wsReconnectStop() @inactiveTimeout.stop()
if @idleTimeout
@idleTimeout.stop()
# stop ws connection # stop ws connection
@io.close() @io.close()
wsReconnectStart: =>
@wsReconnectStop()
if @reconnectDelayId
clearTimeout(@reconnectDelayId)
@reconnectDelayId = setTimeout(@io.connect(), 5000)
wsReconnectStop: =>
if @reconnectDelayId
clearTimeout(@reconnectDelayId)
reconnect: => reconnect: =>
# set status to connecting # set status to connecting
@log.notice 'reconnecting' @log.notice 'reconnecting'
@ -702,24 +718,25 @@ do($ = window.jQuery, window) ->
@el.find('.zammad-chat-body').html @view('customer_timeout') @el.find('.zammad-chat-body').html @view('customer_timeout')
agent: @agent.name agent: @agent.name
delay: @options.inactiveTimeout delay: @options.inactiveTimeout
@close()
reload = -> reload = ->
location.reload() location.reload()
@el.find('.js-restart').click reload @el.find('.js-restart').click reload
@sessionClose()
showWaitingListTimeout: -> showWaitingListTimeout: ->
@el.find('.zammad-chat-body').html @view('waiting_list_timeout') @el.find('.zammad-chat-body').html @view('waiting_list_timeout')
delay: @options.watingListTimeout delay: @options.watingListTimeout
@close()
reload = -> reload = ->
location.reload() location.reload()
@el.find('.js-restart').click reload @el.find('.js-restart').click reload
@sessionClose()
showLoader: -> showLoader: ->
@el.find('.zammad-chat-body').html @view('loader')() @el.find('.zammad-chat-body').html @view('loader')()
setAgentOnlineState: (state) => setAgentOnlineState: (state) =>
@state = state @state = state
return if !@el
capitalizedState = state.charAt(0).toUpperCase() + state.slice(1) capitalizedState = state.charAt(0).toUpperCase() + state.slice(1)
@el @el
.find('.zammad-chat-agent-status') .find('.zammad-chat-agent-status')
@ -757,8 +774,7 @@ do($ = window.jQuery, window) ->
timeoutIntervallCheck: @options.idleTimeoutIntervallCheck timeoutIntervallCheck: @options.idleTimeoutIntervallCheck
callback: => callback: =>
@log.debug 'Idle timeout reached, hide widget', new Date @log.debug 'Idle timeout reached, hide widget', new Date
@state = 'off' @destroy(remove: true)
@destroy(hide: true)
) )
@inactiveTimeout = new Timeout( @inactiveTimeout = new Timeout(
logPrefix: 'inactiveTimeout' logPrefix: 'inactiveTimeout'
@ -767,10 +783,8 @@ do($ = window.jQuery, window) ->
timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck
callback: => callback: =>
@log.debug 'Inactive timeout reached, show timeout screen.', new Date @log.debug 'Inactive timeout reached, show timeout screen.', new Date
@state = 'off'
@setAgentOnlineState 'offline'
@showCustomerTimeout() @showCustomerTimeout()
@destroy(hide:false) @destroy(remove: false)
) )
@waitingListTimeout = new Timeout( @waitingListTimeout = new Timeout(
logPrefix: 'waitingListTimeout' logPrefix: 'waitingListTimeout'
@ -779,10 +793,8 @@ do($ = window.jQuery, window) ->
timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck
callback: => callback: =>
@log.debug 'Waiting list timeout reached, show timeout screen.', new Date @log.debug 'Waiting list timeout reached, show timeout screen.', new Date
@state = 'off'
@setAgentOnlineState 'offline'
@showWaitingListTimeout() @showWaitingListTimeout()
@destroy(hide:false) @destroy(remove: false)
) )
window.ZammadChat = ZammadChat window.ZammadChat = ZammadChat

View file

@ -12,7 +12,8 @@
display: none; display: none;
-webkit-flex-direction: column; -webkit-flex-direction: column;
-ms-flex-direction: column; -ms-flex-direction: column;
flex-direction: column; } flex-direction: column;
z-index: 999; }
.zammad-chat.is-fullscreen { .zammad-chat.is-fullscreen {
right: 0; right: 0;
width: 100%; width: 100%;
@ -106,7 +107,7 @@
margin: 0 1em; margin: 0 1em;
display: inline-block; display: inline-block;
line-height: 2em; line-height: 2em;
padding: 0 0.7em; padding: 0 .7em;
border-radius: 1em; border-radius: 1em;
background: rgba(0, 0, 0, 0.1); background: rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04) inset; } box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04) inset; }
@ -349,6 +350,10 @@
.zammad-chat-send { .zammad-chat-send {
float: right; } float: right; }
.zammad-chat-button:disabled,
.zammad-chat-input:disabled {
opacity: 0.3; }
.zammad-chat-is-hidden { .zammad-chat-is-hidden {
display: none; } display: none; }

View file

@ -128,7 +128,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
if (!this.intervallId) { if (!this.intervallId) {
return; return;
} }
this.log.debug("Stop timeout of " + this.options.timeout + " minutes at"); this.log.debug("Stop timeout of " + this.options.timeout + " minutes");
return clearInterval(this.intervallId); return clearInterval(this.intervallId);
}; };
@ -164,7 +164,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.ws = new window.WebSocket("" + this.options.host); this.ws = new window.WebSocket("" + this.options.host);
this.ws.onopen = (function(_this) { this.ws.onopen = (function(_this) {
return function(e) { return function(e) {
_this.log.debug('on open', e); _this.log.debug('onOpen', e);
return _this.options.onOpen(e); return _this.options.onOpen(e);
}; };
})(this); })(this);
@ -172,7 +172,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
return function(e) { return function(e) {
var pipes; var pipes;
pipes = JSON.parse(e.data); pipes = JSON.parse(e.data);
_this.log.debug('on message', e.data); _this.log.debug('onMessage', e.data);
if (_this.options.onMessage) { if (_this.options.onMessage) {
return _this.options.onMessage(pipes); return _this.options.onMessage(pipes);
} }
@ -180,15 +180,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
})(this); })(this);
this.ws.onclose = (function(_this) { this.ws.onclose = (function(_this) {
return function(e) { return function(e) {
_this.log.debug('close websocket connection'); _this.log.debug('close websocket connection', e);
if (_this.options.onClose) { if (_this.manualClose) {
return _this.options.onClose(e); _this.log.debug('manual close, onClose callback');
_this.manualClose = false;
if (_this.options.onClose) {
return _this.options.onClose(e);
}
} else {
_this.log.debug('error close, onError callback');
if (_this.options.onError) {
return _this.options.onError('Connection lost...');
}
} }
}; };
})(this); })(this);
return this.ws.onerror = (function(_this) { return this.ws.onerror = (function(_this) {
return function(e) { return function(e) {
_this.log.debug('onerror', e); _this.log.debug('onError', e);
if (_this.options.onError) { if (_this.options.onError) {
return _this.options.onError(e); return _this.options.onError(e);
} }
@ -197,11 +206,14 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}; };
Io.prototype.close = function() { Io.prototype.close = function() {
this.log.debug('close websocket manually');
this.manualClose = true;
return this.ws.close(); return this.ws.close();
}; };
Io.prototype.reconnect = function() { Io.prototype.reconnect = function() {
this.ws.close(); this.log.debug('reconnect');
this.close();
return this.connect(); return this.connect();
}; };
@ -238,7 +250,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
buttonClass: 'open-zammad-chat', buttonClass: 'open-zammad-chat',
inactiveClass: 'is-inactive', inactiveClass: 'is-inactive',
title: '<strong>Chat</strong> with us!', title: '<strong>Chat</strong> with us!',
idleTimeout: 8, idleTimeout: 6,
idleTimeoutIntervallCheck: 0.5, idleTimeoutIntervallCheck: 0.5,
inactiveTimeout: 8, inactiveTimeout: 8,
inactiveTimeoutIntervallCheck: 0.5, inactiveTimeoutIntervallCheck: 0.5,
@ -250,7 +262,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
ZammadChat.prototype._messageCount = 0; ZammadChat.prototype._messageCount = 0;
ZammadChat.prototype.isOpen = true; ZammadChat.prototype.isOpen = false;
ZammadChat.prototype.blinkOnlineInterval = null; ZammadChat.prototype.blinkOnlineInterval = null;
@ -336,26 +348,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.setSessionId = bind(this.setSessionId, this); this.setSessionId = bind(this.setSessionId, this);
this.onConnectionReestablished = bind(this.onConnectionReestablished, this); this.onConnectionReestablished = bind(this.onConnectionReestablished, this);
this.reconnect = bind(this.reconnect, 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.destroy = bind(this.destroy, this);
this.onLeaveTemporary = bind(this.onLeaveTemporary, this); this.onLeaveTemporary = bind(this.onLeaveTemporary, this);
this.onAgentTypingEnd = bind(this.onAgentTypingEnd, this); this.onAgentTypingEnd = bind(this.onAgentTypingEnd, this);
this.onAgentTypingStart = bind(this.onAgentTypingStart, this); this.onAgentTypingStart = bind(this.onAgentTypingStart, this);
this.onQueue = bind(this.onQueue, this); this.onQueue = bind(this.onQueue, this);
this.onQueueScreen = bind(this.onQueueScreen, this); this.onQueueScreen = bind(this.onQueueScreen, this);
this.onWebSocketClose = bind(this.onWebSocketClose, this);
this.onCloseAnimationEnd = bind(this.onCloseAnimationEnd, this); this.onCloseAnimationEnd = bind(this.onCloseAnimationEnd, this);
this.closeWindow = bind(this.closeWindow, this);
this.close = bind(this.close, this); this.close = bind(this.close, this);
this.sessionClose = bind(this.sessionClose, this);
this.onOpenAnimationEnd = bind(this.onOpenAnimationEnd, this); this.onOpenAnimationEnd = bind(this.onOpenAnimationEnd, this);
this.open = bind(this.open, this); this.open = bind(this.open, this);
this.renderMessage = bind(this.renderMessage, this); this.renderMessage = bind(this.renderMessage, this);
this.receiveMessage = bind(this.receiveMessage, this); this.receiveMessage = bind(this.receiveMessage, this);
this.onSubmit = bind(this.onSubmit, this); this.onSubmit = bind(this.onSubmit, this);
this.onInput = bind(this.onInput, this); this.onInput = bind(this.onInput, this);
this.reopenSession = bind(this.reopenSession, this); this.onReopenSession = bind(this.onReopenSession, this);
this.onError = bind(this.onError, this); this.onError = bind(this.onError, this);
this.onReady = bind(this.onReady, this);
this.onWebSocketMessage = bind(this.onWebSocketMessage, this); this.onWebSocketMessage = bind(this.onWebSocketMessage, this);
this.send = bind(this.send, this); this.send = bind(this.send, this);
this.checkForEnter = bind(this.checkForEnter, this); this.checkForEnter = bind(this.checkForEnter, this);
@ -393,31 +403,42 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.io = new Io(this.options); this.io = new Io(this.options);
this.io.set({ this.io.set({
onOpen: this.render, onOpen: this.render,
onClose: this.hide, onClose: this.onWebSocketClose,
onMessage: this.onWebSocketMessage onMessage: this.onWebSocketMessage,
onError: this.onError
}); });
this.io.connect(); this.io.connect();
} }
ZammadChat.prototype.render = function() { ZammadChat.prototype.render = function() {
this.el = $(this.view('chat')({ if (!this.el || !$('.zammad-chat').get(0)) {
title: this.options.title this.el = $(this.view('chat')({
})); title: this.options.title
this.options.target.append(this.el); }));
this.input = this.el.find('.zammad-chat-input'); this.options.target.append(this.el);
this.input = this.el.find('.zammad-chat-input');
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
});
$(window).on('beforeunload', (function(_this) {
return function() {
return _this.onLeaveTemporary();
};
})(this));
$(window).bind('hashchange', (function(_this) {
return function() {
if (_this.isOpen) {
return;
}
return _this.idleTimeout.start();
};
})(this));
}
$("." + this.options.buttonClass).addClass(this.inactiveClass); $("." + 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
});
$(window).on('beforeunload', (function(_this) {
return function() {
return _this.onLeaveTemporary();
};
})(this));
this.setAgentOnlineState('online'); this.setAgentOnlineState('online');
this.log.debug('widget rendered'); this.log.debug('widget rendered');
this.startTimeoutObservers(); this.startTimeoutObservers();
@ -453,7 +474,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.log.notice(pipe.data); this.log.notice(pipe.data);
if (pipe.data && pipe.data.state === 'chat_disabled') { if (pipe.data && pipe.data.state === 'chat_disabled') {
this.destroy({ this.destroy({
hide: true remove: true
}); });
} }
break; break;
@ -489,28 +510,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
break; break;
case 'offline': case 'offline':
this.onError('Zammad Chat: No agent online'); this.onError('Zammad Chat: No agent online');
this.state = 'off';
this.destroy({
hide: true
});
break; break;
case 'chat_disabled': case 'chat_disabled':
this.onError('Zammad Chat: Chat is disabled'); this.onError('Zammad Chat: Chat is disabled');
this.state = 'off';
this.destroy({
hide: true
});
break; break;
case 'no_seats_available': case 'no_seats_available':
this.onError("Zammad Chat: Too many clients in queue. Clients in queue: " + pipe.data.queue); this.onError("Zammad Chat: Too many clients in queue. Clients in queue: " + pipe.data.queue);
this.state = 'off';
this.destroy({
hide: true
});
break; break;
case 'reconnect': case 'reconnect':
this.log.debug('old messages', pipe.data.session); this.onReopenSession(pipe.data);
this.reopenSession(pipe.data);
} }
} }
} }
@ -526,11 +534,23 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
ZammadChat.prototype.onError = function(message) { ZammadChat.prototype.onError = function(message) {
this.log.debug(message); this.log.debug(message);
return $("." + this.options.buttonClass).hide(); this.addStatus(message);
$("." + this.options.buttonClass).hide();
if (this.isOpen) {
this.disableInput();
return this.destroy({
remove: false
});
} else {
return this.destroy({
remove: true
});
}
}; };
ZammadChat.prototype.reopenSession = function(data) { ZammadChat.prototype.onReopenSession = function(data) {
var i, len, message, ref, unfinishedMessage; var i, len, message, ref, unfinishedMessage;
this.log.debug('old messages', data.session);
this.inactiveTimeout.start(); this.inactiveTimeout.start();
unfinishedMessage = sessionStorage.getItem('unfinished_message'); unfinishedMessage = sessionStorage.getItem('unfinished_message');
if (data.agent) { if (data.agent) {
@ -631,10 +651,12 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}; };
ZammadChat.prototype.open = function() { ZammadChat.prototype.open = function() {
this.log.debug('open widget');
if (this.isOpen) { if (this.isOpen) {
this.show(); this.log.debug('widget already open, block');
return;
} }
this.isOpen = true;
this.log.debug('open widget');
if (!this.sessionId) { if (!this.sessionId) {
this.showLoader(); this.showLoader();
} }
@ -643,13 +665,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.el.animate({ this.el.animate({
bottom: 0 bottom: 0
}, 500, this.onOpenAnimationEnd); }, 500, this.onOpenAnimationEnd);
return this.send('chat_session_init');
} else { } else {
this.el.css('bottom', 0); this.el.css('bottom', 0);
this.onOpenAnimationEnd(); return this.onOpenAnimationEnd();
}
this.isOpen = true;
if (!this.sessionId) {
return this.send('chat_session_init');
} }
}; };
@ -657,17 +676,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
return this.idleTimeout.stop(); return this.idleTimeout.stop();
}; };
ZammadChat.prototype.close = function(event) { ZammadChat.prototype.sessionClose = function() {
this.log.debug('close widget');
if (this.state === 'off' || this.state === 'unsupported') {
return this.state;
}
if (event) {
event.stopPropagation();
}
if (!this.sessionId) {
return;
}
this.send('chat_session_close', { this.send('chat_session_close', {
session_id: this.sessionId session_id: this.sessionId
}); });
@ -677,14 +686,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
if (this.onInitialQueueDelayId) { if (this.onInitialQueueDelayId) {
clearTimeout(this.onInitialQueueDelayId); clearTimeout(this.onInitialQueueDelayId);
} }
if (event) {
this.closeWindow();
}
return this.setSessionId(void 0); return this.setSessionId(void 0);
}; };
ZammadChat.prototype.closeWindow = function() { ZammadChat.prototype.close = function(event) {
var remainerHeight; var remainerHeight;
if (!this.isOpen) {
this.log.debug('can\'t close widget, it\'s not open');
return;
}
if (!this.sessionId) {
this.log.debug('can\'t close widget without sessionId');
return;
}
this.log.debug('close widget');
if (event) {
event.stopPropagation();
}
this.sessionClose();
this.el.removeClass('zammad-chat-is-open'); this.el.removeClass('zammad-chat-is-open');
remainerHeight = this.el.height() - this.el.find('.zammad-chat-header').outerHeight(); remainerHeight = this.el.height() - this.el.find('.zammad-chat-header').outerHeight();
return this.el.animate({ return this.el.animate({
@ -702,7 +721,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
return this.io.reconnect(); return this.io.reconnect();
}; };
ZammadChat.prototype.hide = function() { ZammadChat.prototype.onWebSocketClose = function() {
if (this.isOpen) {
return;
}
if (this.el) { if (this.el) {
return this.el.removeClass('zammad-chat-is-shown'); return this.el.removeClass('zammad-chat-is-shown');
} }
@ -710,8 +732,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
ZammadChat.prototype.show = function() { ZammadChat.prototype.show = function() {
var remainerHeight; var remainerHeight;
if (this.state === 'off' || this.state === 'unsupported') { if (this.state === 'offline') {
return this.state; return;
} }
this.el.addClass('zammad-chat-is-shown'); this.el.addClass('zammad-chat-is-shown');
if (!this.inputInitialized) { if (!this.inputInitialized) {
@ -809,6 +831,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}; };
ZammadChat.prototype.updateLastTimestamp = function(label, time) { ZammadChat.prototype.updateLastTimestamp = function(label, time) {
if (!this.el) {
return;
}
return this.el.find('.zammad-chat-body').find('.zammad-chat-timestamp').last().replaceWith(this.view('timestamp')({ return this.el.find('.zammad-chat-body').find('.zammad-chat-timestamp').last().replaceWith(this.view('timestamp')({
label: label, label: label,
time: time time: time
@ -816,6 +841,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}; };
ZammadChat.prototype.addStatus = function(status) { ZammadChat.prototype.addStatus = function(status) {
if (!this.el) {
return;
}
this.maybeAddTimestamp(); this.maybeAddTimestamp();
this.el.find('.zammad-chat-body').append(this.view('status')({ this.el.find('.zammad-chat-body').append(this.view('status')({
status: status status: status
@ -831,33 +859,23 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
if (params == null) { if (params == null) {
params = {}; params = {};
} }
this.log.debug('destroy widget'); this.log.debug('destroy widget', params);
if (params.hide) { this.setAgentOnlineState('offline');
if (this.el) { if (params.remove && this.el) {
this.el.remove(); this.el.remove();
} }
if (this.waitingListTimeout) {
this.waitingListTimeout.stop();
}
if (this.inactiveTimeout) {
this.inactiveTimeout.stop();
}
if (this.idleTimeout) {
this.idleTimeout.stop();
} }
this.waitingListTimeout.stop();
this.inactiveTimeout.stop();
this.idleTimeout.stop();
this.wsReconnectStop();
return this.io.close(); return this.io.close();
}; };
ZammadChat.prototype.wsReconnectStart = function() {
this.wsReconnectStop();
if (this.reconnectDelayId) {
clearTimeout(this.reconnectDelayId);
}
return this.reconnectDelayId = setTimeout(this.io.connect(), 5000);
};
ZammadChat.prototype.wsReconnectStop = function() {
if (this.reconnectDelayId) {
return clearTimeout(this.reconnectDelayId);
}
};
ZammadChat.prototype.reconnect = function() { ZammadChat.prototype.reconnect = function() {
this.log.notice('reconnecting'); this.log.notice('reconnecting');
this.disableInput(); this.disableInput();
@ -920,11 +938,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
agent: this.agent.name, agent: this.agent.name,
delay: this.options.inactiveTimeout delay: this.options.inactiveTimeout
})); }));
this.close();
reload = function() { reload = function() {
return location.reload(); return location.reload();
}; };
return this.el.find('.js-restart').click(reload); this.el.find('.js-restart').click(reload);
return this.sessionClose();
}; };
ZammadChat.prototype.showWaitingListTimeout = function() { ZammadChat.prototype.showWaitingListTimeout = function() {
@ -932,11 +950,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.el.find('.zammad-chat-body').html(this.view('waiting_list_timeout')({ this.el.find('.zammad-chat-body').html(this.view('waiting_list_timeout')({
delay: this.options.watingListTimeout delay: this.options.watingListTimeout
})); }));
this.close();
reload = function() { reload = function() {
return location.reload(); return location.reload();
}; };
return this.el.find('.js-restart').click(reload); this.el.find('.js-restart').click(reload);
return this.sessionClose();
}; };
ZammadChat.prototype.showLoader = function() { ZammadChat.prototype.showLoader = function() {
@ -946,6 +964,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
ZammadChat.prototype.setAgentOnlineState = function(state) { ZammadChat.prototype.setAgentOnlineState = function(state) {
var capitalizedState; var capitalizedState;
this.state = state; this.state = state;
if (!this.el) {
return;
}
capitalizedState = state.charAt(0).toUpperCase() + state.slice(1); capitalizedState = state.charAt(0).toUpperCase() + state.slice(1);
return this.el.find('.zammad-chat-agent-status').attr('data-status', state).text(this.T(capitalizedState)); return this.el.find('.zammad-chat-agent-status').attr('data-status', state).text(this.T(capitalizedState));
}; };
@ -986,9 +1007,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
callback: (function(_this) { callback: (function(_this) {
return function() { return function() {
_this.log.debug('Idle timeout reached, hide widget', new Date); _this.log.debug('Idle timeout reached, hide widget', new Date);
_this.state = 'off';
return _this.destroy({ return _this.destroy({
hide: true remove: true
}); });
}; };
})(this) })(this)
@ -1001,11 +1021,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
callback: (function(_this) { callback: (function(_this) {
return function() { return function() {
_this.log.debug('Inactive timeout reached, show timeout screen.', new Date); _this.log.debug('Inactive timeout reached, show timeout screen.', new Date);
_this.state = 'off';
_this.setAgentOnlineState('offline');
_this.showCustomerTimeout(); _this.showCustomerTimeout();
return _this.destroy({ return _this.destroy({
hide: false remove: false
}); });
}; };
})(this) })(this)
@ -1018,11 +1036,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
callback: (function(_this) { callback: (function(_this) {
return function() { return function() {
_this.log.debug('Waiting list timeout reached, show timeout screen.', new Date); _this.log.debug('Waiting list timeout reached, show timeout screen.', new Date);
_this.state = 'off';
_this.setAgentOnlineState('offline');
_this.showWaitingListTimeout(); _this.showWaitingListTimeout();
return _this.destroy({ return _this.destroy({
hide: false remove: false
}); });
}; };
})(this) })(this)
@ -1035,67 +1051,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
return window.ZammadChat = ZammadChat; return window.ZammadChat = ZammadChat;
})(window.jQuery, window); })(window.jQuery, window);
if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {};
}
window.zammadChatTemplates["agent"] = function (__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
if (this.agent.avatar) {
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
__out.push(__sanitize(this.agent.avatar));
__out.push('">\n');
}
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
__out.push(__sanitize(this.agent.name));
__out.push('</span>\n</span>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
/*! /*!
* ---------------------------------------------------------------------------- * ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42): * "THE BEER-WARE LICENSE" (Revision 42):
@ -1181,6 +1136,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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
if (this.agent.avatar) {
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
__out.push(__sanitize(this.agent.avatar));
__out.push('">\n');
}
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
__out.push(__sanitize(this.agent.name));
__out.push('</span>\n</span>');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
};
if (!window.zammadChatTemplates) { if (!window.zammadChatTemplates) {
window.zammadChatTemplates = {}; window.zammadChatTemplates = {};
} }

File diff suppressed because one or more lines are too long

View file

@ -11,6 +11,7 @@
will-change: bottom; will-change: bottom;
display: none; display: none;
flex-direction: column; flex-direction: column;
z-index: 999;
&.is-fullscreen { &.is-fullscreen {
right: 0; right: 0;
@ -177,7 +178,7 @@
.zammad-chat-modal-text { .zammad-chat-modal-text {
font-size: 1.3em; font-size: 1.3em;
line-height: 1.45; line-height: 1.45;
.zammad-chat-loading-animation { .zammad-chat-loading-animation {
font-size: 0.7em; font-size: 0.7em;
} }
@ -198,7 +199,7 @@
overflow: auto; overflow: auto;
background: white; background: white;
flex: 1; flex: 1;
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
height: auto; height: auto;
flex: 1; flex: 1;
@ -351,6 +352,11 @@
float: right; float: right;
} }
.zammad-chat-button:disabled,
.zammad-chat-input:disabled {
opacity: 0.3;
}
.zammad-chat-is-hidden { .zammad-chat-is-hidden {
display: none; display: none;
} }

View file

@ -387,6 +387,42 @@ class ChatTest < TestCase
css: '.zammad-chat', css: '.zammad-chat',
value: 'Chat closed by', value: 'Chat closed by',
) )
click(
browser: customer,
css: '.zammad-chat .js-chat-close',
)
watch_for_disappear(
browser: customer,
css: '.zammad-chat-is-open',
)
agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click)
sleep 2
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',
)
exists(
browser: agent,
css: '.active .chat-window .chat-status',
)
end end
def test_basic_usecase3 def test_basic_usecase3
@ -579,7 +615,7 @@ class ChatTest < TestCase
browser: customer, browser: customer,
css: '.zammad-chat', css: '.zammad-chat',
value: '(takes longer|dauert länger)', value: '(takes longer|dauert länger)',
timeout: 90, timeout: 120,
) )
# no customer action, show sorry screen # no customer action, show sorry screen
@ -625,6 +661,55 @@ class ChatTest < TestCase
timeout: 150, timeout: 150,
) )
agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click)
sleep 2
click(
browser: customer,
css: '.js-restart',
)
sleep 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(
browser: agent,
css: '.active .chat-window .chat-status',
)
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',
)
end end
end end