Просмотр исходного кода

Fixed timing issues, improved tests.

Martin Edenhofer 9 лет назад
Родитель
Сommit
f06348e843

+ 19 - 8
app/assets/javascripts/app/controllers/chat.coffee

@@ -291,7 +291,7 @@ class ChatWindow extends App.Controller
             phrasesArray = phrases.split(';')
             phrase = phrasesArray[_.random(0, phrasesArray.length-1)]
             @input.html(phrase)
-            @sendMessage()
+            @sendMessage(1600)
 
   focus: =>
     @input.focus()
@@ -372,16 +372,27 @@ class ChatWindow extends App.Controller
           event.preventDefault()
           @sendMessage()
 
-  sendMessage: =>
+  sendMessage: (delay) =>
     content = @input.html()
     return if !content
 
-    App.WebSocket.send(
-      event:'chat_session_message'
-      data:
-        content: content
-        session_id: @session.session_id
-    )
+    send = =>
+      App.WebSocket.send(
+        event:'chat_session_message'
+        data:
+          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'
     @input.html('')

+ 100 - 88
public/assets/chat/chat.coffee

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

+ 7 - 2
public/assets/chat/chat.css

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

+ 187 - 171
public/assets/chat/chat.js

@@ -128,7 +128,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
       if (!this.intervallId) {
         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);
     };
 
@@ -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.onopen = (function(_this) {
         return function(e) {
-          _this.log.debug('on open', e);
+          _this.log.debug('onOpen', e);
           return _this.options.onOpen(e);
         };
       })(this);
@@ -172,7 +172,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
         return function(e) {
           var pipes;
           pipes = JSON.parse(e.data);
-          _this.log.debug('on message', e.data);
+          _this.log.debug('onMessage', e.data);
           if (_this.options.onMessage) {
             return _this.options.onMessage(pipes);
           }
@@ -180,15 +180,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
       })(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.log.debug('close websocket connection', e);
+          if (_this.manualClose) {
+            _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);
       return this.ws.onerror = (function(_this) {
         return function(e) {
-          _this.log.debug('onerror', e);
+          _this.log.debug('onError', e);
           if (_this.options.onError) {
             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() {
+      this.log.debug('close websocket manually');
+      this.manualClose = true;
       return this.ws.close();
     };
 
     Io.prototype.reconnect = function() {
-      this.ws.close();
+      this.log.debug('reconnect');
+      this.close();
       return this.connect();
     };
 
@@ -238,7 +250,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
       buttonClass: 'open-zammad-chat',
       inactiveClass: 'is-inactive',
       title: '<strong>Chat</strong> with us!',
-      idleTimeout: 8,
+      idleTimeout: 6,
       idleTimeoutIntervallCheck: 0.5,
       inactiveTimeout: 8,
       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.isOpen = true;
+    ZammadChat.prototype.isOpen = false;
 
     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.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.onWebSocketClose = bind(this.onWebSocketClose, this);
       this.onCloseAnimationEnd = bind(this.onCloseAnimationEnd, this);
-      this.closeWindow = bind(this.closeWindow, this);
       this.close = bind(this.close, this);
+      this.sessionClose = bind(this.sessionClose, 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.onReopenSession = bind(this.onReopenSession, 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);
@@ -393,31 +403,42 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
       this.io = new Io(this.options);
       this.io.set({
         onOpen: this.render,
-        onClose: this.hide,
-        onMessage: this.onWebSocketMessage
+        onClose: this.onWebSocketClose,
+        onMessage: this.onWebSocketMessage,
+        onError: this.onError
       });
       this.io.connect();
     }
 
     ZammadChat.prototype.render = function() {
-      this.el = $(this.view('chat')({
-        title: this.options.title
-      }));
-      this.options.target.append(this.el);
-      this.input = this.el.find('.zammad-chat-input');
+      if (!this.el || !$('.zammad-chat').get(0)) {
+        this.el = $(this.view('chat')({
+          title: this.options.title
+        }));
+        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.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.log.debug('widget rendered');
       this.startTimeoutObservers();
@@ -453,7 +474,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
             this.log.notice(pipe.data);
             if (pipe.data && pipe.data.state === 'chat_disabled') {
               this.destroy({
-                hide: true
+                remove: true
               });
             }
             break;
@@ -489,28 +510,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
                 break;
               case 'offline':
                 this.onError('Zammad Chat: No agent online');
-                this.state = 'off';
-                this.destroy({
-                  hide: true
-                });
                 break;
               case 'chat_disabled':
                 this.onError('Zammad Chat: Chat is disabled');
-                this.state = 'off';
-                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.destroy({
-                  hide: true
-                });
                 break;
               case 'reconnect':
-                this.log.debug('old messages', pipe.data.session);
-                this.reopenSession(pipe.data);
+                this.onReopenSession(pipe.data);
             }
         }
       }
@@ -526,11 +534,23 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
 
     ZammadChat.prototype.onError = function(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;
+      this.log.debug('old messages', data.session);
       this.inactiveTimeout.start();
       unfinishedMessage = sessionStorage.getItem('unfinished_message');
       if (data.agent) {
@@ -631,10 +651,12 @@ 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();
+        this.log.debug('widget already open, block');
+        return;
       }
+      this.isOpen = true;
+      this.log.debug('open widget');
       if (!this.sessionId) {
         this.showLoader();
       }
@@ -643,13 +665,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
         this.el.animate({
           bottom: 0
         }, 500, this.onOpenAnimationEnd);
+        return this.send('chat_session_init');
       } else {
         this.el.css('bottom', 0);
-        this.onOpenAnimationEnd();
-      }
-      this.isOpen = true;
-      if (!this.sessionId) {
-        return this.send('chat_session_init');
+        return this.onOpenAnimationEnd();
       }
     };
 
@@ -657,17 +676,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
       return this.idleTimeout.stop();
     };
 
-    ZammadChat.prototype.close = function(event) {
-      this.log.debug('close widget');
-      if (this.state === 'off' || this.state === 'unsupported') {
-        return this.state;
-      }
-      if (event) {
-        event.stopPropagation();
-      }
-      if (!this.sessionId) {
-        return;
-      }
+    ZammadChat.prototype.sessionClose = function() {
       this.send('chat_session_close', {
         session_id: this.sessionId
       });
@@ -677,14 +686,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
       if (this.onInitialQueueDelayId) {
         clearTimeout(this.onInitialQueueDelayId);
       }
-      if (event) {
-        this.closeWindow();
-      }
       return this.setSessionId(void 0);
     };
 
-    ZammadChat.prototype.closeWindow = function() {
+    ZammadChat.prototype.close = function(event) {
       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');
       remainerHeight = this.el.height() - this.el.find('.zammad-chat-header').outerHeight();
       return this.el.animate({
@@ -702,7 +721,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
       return this.io.reconnect();
     };
 
-    ZammadChat.prototype.hide = function() {
+    ZammadChat.prototype.onWebSocketClose = function() {
+      if (this.isOpen) {
+        return;
+      }
       if (this.el) {
         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() {
       var remainerHeight;
-      if (this.state === 'off' || this.state === 'unsupported') {
-        return this.state;
+      if (this.state === 'offline') {
+        return;
       }
       this.el.addClass('zammad-chat-is-shown');
       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) {
+      if (!this.el) {
+        return;
+      }
       return this.el.find('.zammad-chat-body').find('.zammad-chat-timestamp').last().replaceWith(this.view('timestamp')({
         label: label,
         time: time
@@ -816,6 +841,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
     };
 
     ZammadChat.prototype.addStatus = function(status) {
+      if (!this.el) {
+        return;
+      }
       this.maybeAddTimestamp();
       this.el.find('.zammad-chat-body').append(this.view('status')({
         status: status
@@ -831,31 +859,21 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
       if (params == null) {
         params = {};
       }
-      this.log.debug('destroy widget');
-      if (params.hide) {
-        if (this.el) {
-          this.el.remove();
-        }
+      this.log.debug('destroy widget', params);
+      this.setAgentOnlineState('offline');
+      if (params.remove && this.el) {
+        this.el.remove();
       }
-      this.waitingListTimeout.stop();
-      this.inactiveTimeout.stop();
-      this.idleTimeout.stop();
-      this.wsReconnectStop();
-      return this.io.close();
-    };
-
-    ZammadChat.prototype.wsReconnectStart = function() {
-      this.wsReconnectStop();
-      if (this.reconnectDelayId) {
-        clearTimeout(this.reconnectDelayId);
+      if (this.waitingListTimeout) {
+        this.waitingListTimeout.stop();
       }
-      return this.reconnectDelayId = setTimeout(this.io.connect(), 5000);
-    };
-
-    ZammadChat.prototype.wsReconnectStop = function() {
-      if (this.reconnectDelayId) {
-        return clearTimeout(this.reconnectDelayId);
+      if (this.inactiveTimeout) {
+        this.inactiveTimeout.stop();
       }
+      if (this.idleTimeout) {
+        this.idleTimeout.stop();
+      }
+      return this.io.close();
     };
 
     ZammadChat.prototype.reconnect = function() {
@@ -920,11 +938,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
         agent: this.agent.name,
         delay: this.options.inactiveTimeout
       }));
-      this.close();
       reload = function() {
         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() {
@@ -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')({
         delay: this.options.watingListTimeout
       }));
-      this.close();
       reload = function() {
         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() {
@@ -946,6 +964,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
     ZammadChat.prototype.setAgentOnlineState = function(state) {
       var capitalizedState;
       this.state = state;
+      if (!this.el) {
+        return;
+      }
       capitalizedState = state.charAt(0).toUpperCase() + state.slice(1);
       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) {
           return function() {
             _this.log.debug('Idle timeout reached, hide widget', new Date);
-            _this.state = 'off';
             return _this.destroy({
-              hide: true
+              remove: true
             });
           };
         })(this)
@@ -1001,11 +1021,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
         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
+              remove: false
             });
           };
         })(this)
@@ -1018,11 +1036,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
         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
+              remove: false
             });
           };
         })(this)
@@ -1035,67 +1051,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
   return window.ZammadChat = ZammadChat;
 })(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):
@@ -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) {
   window.zammadChatTemplates = {};
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
public/assets/chat/chat.min.js


+ 8 - 2
public/assets/chat/chat.scss

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

+ 86 - 1
test/browser/chat_test.rb

@@ -387,6 +387,42 @@ class ChatTest < TestCase
       css: '.zammad-chat',
       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
 
   def test_basic_usecase3
@@ -579,7 +615,7 @@ class ChatTest < TestCase
       browser: customer,
       css: '.zammad-chat',
       value: '(takes longer|dauert länger)',
-      timeout: 90,
+      timeout: 120,
     )
 
     # no customer action, show sorry screen
@@ -625,6 +661,55 @@ class ChatTest < TestCase
       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

Некоторые файлы не были показаны из-за большого количества измененных файлов