chat.coffee 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  1. do($ = window.jQuery, window) ->
  2. scripts = document.getElementsByTagName('script')
  3. myScript = scripts[scripts.length - 1]
  4. scriptHost = myScript.src.match('.*://([^:/]*).*')[1]
  5. # Define the plugin class
  6. class Base
  7. defaults:
  8. debug: false
  9. constructor: (options) ->
  10. @options = $.extend {}, @defaults, options
  11. @log = new Log(debug: @options.debug, logPrefix: @options.logPrefix || @logPrefix)
  12. class Log
  13. defaults:
  14. debug: false
  15. constructor: (options) ->
  16. @options = $.extend {}, @defaults, options
  17. debug: (items...) =>
  18. return if !@options.debug
  19. @log('debug', items)
  20. notice: (items...) =>
  21. @log('notice', items)
  22. error: (items...) =>
  23. @log('error', items)
  24. log: (level, items) =>
  25. items.unshift('||')
  26. items.unshift(level)
  27. items.unshift(@options.logPrefix)
  28. console.log.apply console, items
  29. return if !@options.debug
  30. logString = ''
  31. for item in items
  32. logString += ' '
  33. if typeof item is 'object'
  34. logString += JSON.stringify(item)
  35. else if item && item.toString
  36. logString += item.toString()
  37. else
  38. logString += item
  39. $('.js-chatLogDisplay').prepend('<div>' + logString + '</div>')
  40. class Timeout extends Base
  41. timeoutStartedAt: null
  42. logPrefix: 'timeout'
  43. defaults:
  44. debug: false
  45. timeout: 4
  46. timeoutIntervallCheck: 0.5
  47. constructor: (options) ->
  48. super(options)
  49. start: =>
  50. @stop()
  51. timeoutStartedAt = new Date
  52. check = =>
  53. timeLeft = new Date - new Date(timeoutStartedAt.getTime() + @options.timeout * 1000 * 60)
  54. @log.debug "Timeout check for #{@options.timeout} minutes (left #{timeLeft/1000} sec.)"#, new Date
  55. return if timeLeft < 0
  56. @stop()
  57. @options.callback()
  58. @log.debug "Start timeout in #{@options.timeout} minutes"#, new Date
  59. @intervallId = setInterval(check, @options.timeoutIntervallCheck * 1000 * 60)
  60. stop: =>
  61. return if !@intervallId
  62. @log.debug "Stop timeout of #{@options.timeout} minutes"#, new Date
  63. clearInterval(@intervallId)
  64. class Io extends Base
  65. logPrefix: 'io'
  66. constructor: (options) ->
  67. super(options)
  68. set: (params) =>
  69. for key, value of params
  70. @options[key] = value
  71. connect: =>
  72. @log.debug "Connecting to #{@options.host}"
  73. @ws = new window.WebSocket("#{@options.host}")
  74. @ws.onopen = (e) =>
  75. @log.debug 'onOpen', e
  76. @options.onOpen(e)
  77. @ping()
  78. @ws.onmessage = (e) =>
  79. pipes = JSON.parse(e.data)
  80. @log.debug 'onMessage', e.data
  81. for pipe in pipes
  82. if pipe.event is 'pong'
  83. @ping()
  84. if @options.onMessage
  85. @options.onMessage(pipes)
  86. @ws.onclose = (e) =>
  87. @log.debug 'close websocket connection', e
  88. if @pingDelayId
  89. clearTimeout(@pingDelayId)
  90. if @manualClose
  91. @log.debug 'manual close, onClose callback'
  92. @manualClose = false
  93. if @options.onClose
  94. @options.onClose(e)
  95. else
  96. @log.debug 'error close, onError callback'
  97. if @options.onError
  98. @options.onError('Connection lost...')
  99. @ws.onerror = (e) =>
  100. @log.debug 'onError', e
  101. if @options.onError
  102. @options.onError(e)
  103. close: =>
  104. @log.debug 'close websocket manually'
  105. @manualClose = true
  106. @ws.close()
  107. reconnect: =>
  108. @log.debug 'reconnect'
  109. @close()
  110. @connect()
  111. send: (event, data = {}) =>
  112. @log.debug 'send', event, data
  113. msg = JSON.stringify
  114. event: event
  115. data: data
  116. @ws.send msg
  117. ping: =>
  118. localPing = =>
  119. @send('ping')
  120. @pingDelayId = setTimeout(localPing, 29000)
  121. class ZammadChat extends Base
  122. defaults:
  123. chatId: undefined
  124. show: true
  125. target: $('body')
  126. host: ''
  127. debug: false
  128. flat: false
  129. lang: undefined
  130. cssAutoload: true
  131. cssUrl: undefined
  132. fontSize: undefined
  133. buttonClass: 'open-zammad-chat'
  134. inactiveClass: 'is-inactive'
  135. title: '<strong>Chat</strong> with us!'
  136. scrollHint: 'Scrolle nach unten um neue Nachrichten zu sehen'
  137. idleTimeout: 6
  138. idleTimeoutIntervallCheck: 0.5
  139. inactiveTimeout: 8
  140. inactiveTimeoutIntervallCheck: 0.5
  141. waitingListTimeout: 4
  142. waitingListTimeoutIntervallCheck: 0.5
  143. logPrefix: 'chat'
  144. _messageCount: 0
  145. isOpen: false
  146. blinkOnlineInterval: null
  147. stopBlinOnlineStateTimeout: null
  148. showTimeEveryXMinutes: 2
  149. lastTimestamp: null
  150. lastAddedType: null
  151. inputTimeout: null
  152. isTyping: false
  153. state: 'offline'
  154. initialQueueDelay: 10000
  155. translations:
  156. de:
  157. '<strong>Chat</strong> with us!': '<strong>Chatte</strong> mit uns!'
  158. 'Scroll down to see new messages': 'Scrolle nach unten um neue Nachrichten zu sehen'
  159. 'Online': 'Online'
  160. 'Online': 'Online'
  161. 'Offline': 'Offline'
  162. 'Connecting': 'Verbinden'
  163. 'Connection re-established': 'Verbindung wiederhergestellt'
  164. 'Today': 'Heute'
  165. 'Send': 'Senden'
  166. 'Compose your message...': 'Ihre Nachricht...'
  167. 'All colleagues are busy.': 'Alle Kollegen sind belegt.'
  168. 'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste an der Position <strong>%s</strong>.'
  169. 'Start new conversation': 'Neue Konversation starten'
  170. 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit <strong>%s</strong> geschlossen.'
  171. '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.'
  172. sessionId: undefined
  173. scrolledToBottom: true
  174. scrollSnapTolerance: 10
  175. T: (string, items...) =>
  176. if @options.lang && @options.lang isnt 'en'
  177. if !@translations[@options.lang]
  178. @log.notice "Translation '#{@options.lang}' needed!"
  179. else
  180. translations = @translations[@options.lang]
  181. if !translations[string]
  182. @log.notice "Translation needed for '#{string}'"
  183. string = translations[string] || string
  184. if items
  185. for item in items
  186. string = string.replace(/%s/, item)
  187. string
  188. view: (name) =>
  189. return (options) =>
  190. if !options
  191. options = {}
  192. options.T = @T
  193. options.background = @options.background
  194. options.flat = @options.flat
  195. options.fontSize = @options.fontSize
  196. return window.zammadChatTemplates[name](options)
  197. constructor: (options) ->
  198. @options = $.extend {}, @defaults, options
  199. super(@options)
  200. # fullscreen
  201. @isFullscreen = (window.matchMedia and window.matchMedia('(max-width: 768px)').matches)
  202. @scrollRoot = $(@getScrollRoot())
  203. # check prerequisites
  204. if !$
  205. @state = 'unsupported'
  206. @log.notice 'Chat: no jquery found!'
  207. return
  208. if !window.WebSocket or !sessionStorage
  209. @state = 'unsupported'
  210. @log.notice 'Chat: Browser not supported!'
  211. return
  212. if !@options.chatId
  213. @state = 'unsupported'
  214. @log.error 'Chat: need chatId as option!'
  215. return
  216. # detect language
  217. if !@options.lang
  218. @options.lang = $('html').attr('lang')
  219. if @options.lang
  220. @options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx
  221. @log.debug "lang: #{@options.lang}"
  222. # detect host
  223. @detectHost() if !@options.host
  224. @loadCss()
  225. @io = new Io(@options)
  226. @io.set(
  227. onOpen: @render
  228. onClose: @onWebSocketClose
  229. onMessage: @onWebSocketMessage
  230. onError: @onError
  231. )
  232. @io.connect()
  233. getScrollRoot: ->
  234. return document.scrollingElement if 'scrollingElement' of document
  235. html = document.documentElement
  236. start = html.scrollTop
  237. html.scrollTop = start + 1
  238. end = html.scrollTop
  239. html.scrollTop = start
  240. return if end > start then html else document.body
  241. render: =>
  242. if !@el || !$('.zammad-chat').get(0)
  243. @renderBase()
  244. # disable open button
  245. $(".#{ @options.buttonClass }").addClass @inactiveClass
  246. @setAgentOnlineState 'online'
  247. @log.debug 'widget rendered'
  248. @startTimeoutObservers()
  249. @idleTimeout.start()
  250. # get current chat status
  251. @sessionId = sessionStorage.getItem('sessionId')
  252. @send 'chat_status_customer',
  253. session_id: @sessionId
  254. url: window.location.href
  255. renderBase: ->
  256. @el = $(@view('chat')(
  257. title: @options.title,
  258. scrollHint: @options.scrollHint
  259. ))
  260. @options.target.append @el
  261. @input = @el.find('.zammad-chat-input')
  262. # start bindings
  263. @el.find('.js-chat-open').click @open
  264. @el.find('.js-chat-toggle').click @toggle
  265. @el.find('.zammad-chat-controls').on 'submit', @onSubmit
  266. @el.find('.zammad-chat-body').on 'scroll', @detectScrolledtoBottom
  267. @el.find('.zammad-scroll-hint').click @onScrollHintClick
  268. @input.on
  269. keydown: @checkForEnter
  270. input: @onInput
  271. $(window).on('beforeunload', =>
  272. @onLeaveTemporary()
  273. )
  274. $(window).bind('hashchange', =>
  275. if @isOpen
  276. if @sessionId
  277. @send 'chat_session_notice',
  278. session_id: @sessionId
  279. message: window.location.href
  280. return
  281. @idleTimeout.start()
  282. )
  283. if @isFullscreen
  284. @input.on
  285. focus: @onFocus
  286. focusout: @onFocusOut
  287. checkForEnter: (event) =>
  288. if not event.shiftKey and event.keyCode is 13
  289. event.preventDefault()
  290. @sendMessage()
  291. send: (event, data = {}) =>
  292. data.chat_id = @options.chatId
  293. @io.send(event, data)
  294. onWebSocketMessage: (pipes) =>
  295. for pipe in pipes
  296. @log.debug 'ws:onmessage', pipe
  297. switch pipe.event
  298. when 'chat_error'
  299. @log.notice pipe.data
  300. if pipe.data && pipe.data.state is 'chat_disabled'
  301. @destroy(remove: true)
  302. when 'chat_session_message'
  303. return if pipe.data.self_written
  304. @receiveMessage pipe.data
  305. when 'chat_session_typing'
  306. return if pipe.data.self_written
  307. @onAgentTypingStart()
  308. when 'chat_session_start'
  309. @onConnectionEstablished pipe.data
  310. when 'chat_session_queue'
  311. @onQueueScreen pipe.data
  312. when 'chat_session_closed'
  313. @onSessionClosed pipe.data
  314. when 'chat_session_left'
  315. @onSessionClosed pipe.data
  316. when 'chat_status_customer'
  317. switch pipe.data.state
  318. when 'online'
  319. @sessionId = undefined
  320. if !@options.cssAutoload || @cssLoaded
  321. @onReady()
  322. else
  323. @socketReady = true
  324. when 'offline'
  325. @onError 'Zammad Chat: No agent online'
  326. when 'chat_disabled'
  327. @onError 'Zammad Chat: Chat is disabled'
  328. when 'no_seats_available'
  329. @onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}"
  330. when 'reconnect'
  331. @onReopenSession pipe.data
  332. onReady: ->
  333. @log.debug 'widget ready for use'
  334. $(".#{ @options.buttonClass }").click(@open).removeClass(@inactiveClass)
  335. if @options.show
  336. @show()
  337. onError: (message) =>
  338. @log.debug message
  339. @addStatus(message)
  340. $(".#{ @options.buttonClass }").hide()
  341. if @isOpen
  342. @disableInput()
  343. @destroy(remove: false)
  344. else
  345. @destroy(remove: true)
  346. onReopenSession: (data) =>
  347. @log.debug 'old messages', data.session
  348. @inactiveTimeout.start()
  349. unfinishedMessage = sessionStorage.getItem 'unfinished_message'
  350. # rerender chat history
  351. if data.agent
  352. @onConnectionEstablished(data)
  353. for message in data.session
  354. @renderMessage
  355. message: message.content
  356. id: message.id
  357. from: if message.created_by_id then 'agent' else 'customer'
  358. if unfinishedMessage
  359. @input.val unfinishedMessage
  360. # show wait list
  361. if data.position
  362. @onQueue data
  363. @show()
  364. @open()
  365. @scrollToBottom()
  366. if unfinishedMessage
  367. @input.focus()
  368. onInput: =>
  369. # remove unread-state from messages
  370. @el.find('.zammad-chat-message--unread')
  371. .removeClass 'zammad-chat-message--unread'
  372. sessionStorage.setItem 'unfinished_message', @input.val()
  373. @onTyping()
  374. onFocus: =>
  375. $(window).scrollTop(10)
  376. keyboardShown = $(window).scrollTop() > 0
  377. $(window).scrollTop(0)
  378. if keyboardShown
  379. @log.notice 'virtual keyboard shown'
  380. # on keyboard shown
  381. # can't measure visible area height :(
  382. onFocusOut: ->
  383. # on keyboard hidden
  384. onTyping: ->
  385. # send typing start event only every 1.5 seconds
  386. return if @isTyping && @isTyping > new Date(new Date().getTime() - 1500)
  387. @isTyping = new Date()
  388. @send 'chat_session_typing',
  389. session_id: @sessionId
  390. @inactiveTimeout.start()
  391. onSubmit: (event) =>
  392. event.preventDefault()
  393. @sendMessage()
  394. sendMessage: ->
  395. message = @input.val()
  396. return if !message
  397. @inactiveTimeout.start()
  398. sessionStorage.removeItem 'unfinished_message'
  399. messageElement = @view('message')
  400. message: message
  401. from: 'customer'
  402. id: @_messageCount++
  403. unreadClass: ''
  404. @maybeAddTimestamp()
  405. # add message before message typing loader
  406. if @el.find('.zammad-chat-message--typing').size()
  407. @lastAddedType = 'typing-placeholder'
  408. @el.find('.zammad-chat-message--typing').before messageElement
  409. else
  410. @lastAddedType = 'message--customer'
  411. @el.find('.zammad-chat-body').append messageElement
  412. @input.val('')
  413. @scrollToBottom()
  414. # send message event
  415. @send 'chat_session_message',
  416. content: message
  417. id: @_messageCount
  418. session_id: @sessionId
  419. receiveMessage: (data) =>
  420. @inactiveTimeout.start()
  421. # hide writing indicator
  422. @onAgentTypingEnd()
  423. @maybeAddTimestamp()
  424. @renderMessage
  425. message: data.message.content
  426. id: data.id
  427. from: 'agent'
  428. @scrollToBottom showHint: true
  429. renderMessage: (data) =>
  430. @lastAddedType = "message--#{ data.from }"
  431. data.unreadClass = if document.hidden then ' zammad-chat-message--unread' else ''
  432. @el.find('.zammad-chat-body').append @view('message')(data)
  433. open: =>
  434. if @isOpen
  435. @log.debug 'widget already open, block'
  436. return
  437. @isOpen = true
  438. @log.debug 'open widget'
  439. if !@sessionId
  440. @showLoader()
  441. @el.addClass('zammad-chat-is-open')
  442. if !@inputInitialized
  443. @inputInitialized = true
  444. @input.autoGrow
  445. extraLine: false
  446. remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
  447. @el.css 'bottom', -remainerHeight
  448. if !@sessionId
  449. @el.animate { bottom: 0 }, 500, @onOpenAnimationEnd
  450. @send('chat_session_init'
  451. url: window.location.href
  452. )
  453. else
  454. @el.css 'bottom', 0
  455. @onOpenAnimationEnd()
  456. onOpenAnimationEnd: =>
  457. @idleTimeout.stop()
  458. if @isFullscreen
  459. @disableScrollOnRoot()
  460. sessionClose: =>
  461. # send close
  462. @send 'chat_session_close',
  463. session_id: @sessionId
  464. # stop timer
  465. @inactiveTimeout.stop()
  466. @waitingListTimeout.stop()
  467. # delete input store
  468. sessionStorage.removeItem 'unfinished_message'
  469. # stop delay of initial queue position
  470. if @onInitialQueueDelayId
  471. clearTimeout(@onInitialQueueDelayId)
  472. @setSessionId undefined
  473. toggle: (event) =>
  474. if @isOpen
  475. @close(event)
  476. else
  477. @open(event)
  478. close: (event) =>
  479. if !@isOpen
  480. @log.debug 'can\'t close widget, it\'s not open'
  481. return
  482. if @initDelayId
  483. clearTimeout(@initDelayId)
  484. if !@sessionId
  485. @log.debug 'can\'t close widget without sessionId'
  486. return
  487. @log.debug 'close widget'
  488. event.stopPropagation() if event
  489. @sessionClose()
  490. if @isFullscreen
  491. @enableScrollOnRoot()
  492. # close window
  493. remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
  494. @el.animate { bottom: -remainerHeight }, 500, @onCloseAnimationEnd
  495. onCloseAnimationEnd: =>
  496. @el.css 'bottom', ''
  497. @el.removeClass('zammad-chat-is-open')
  498. @showLoader()
  499. @el.find('.zammad-chat-welcome').removeClass('zammad-chat-is-hidden')
  500. @el.find('.zammad-chat-agent').addClass('zammad-chat-is-hidden')
  501. @el.find('.zammad-chat-agent-status').addClass('zammad-chat-is-hidden')
  502. @isOpen = false
  503. @io.reconnect()
  504. onWebSocketClose: =>
  505. return if @isOpen
  506. if @el
  507. @el.removeClass('zammad-chat-is-shown')
  508. @el.removeClass('zammad-chat-is-loaded')
  509. show: ->
  510. return if @state is 'offline'
  511. @el.addClass('zammad-chat-is-loaded')
  512. @el.addClass('zammad-chat-is-shown')
  513. disableInput: ->
  514. @input.prop('disabled', true)
  515. @el.find('.zammad-chat-send').prop('disabled', true)
  516. enableInput: ->
  517. @input.prop('disabled', false)
  518. @el.find('.zammad-chat-send').prop('disabled', false)
  519. hideModal: ->
  520. @el.find('.zammad-chat-modal').html ''
  521. onQueueScreen: (data) =>
  522. @setSessionId data.session_id
  523. # delay initial queue position, show connecting first
  524. show = =>
  525. @onQueue data
  526. @waitingListTimeout.start()
  527. if @initialQueueDelay && !@onInitialQueueDelayId
  528. @onInitialQueueDelayId = setTimeout(show, @initialQueueDelay)
  529. return
  530. # stop delay of initial queue position
  531. if @onInitialQueueDelayId
  532. clearTimeout(@onInitialQueueDelayId)
  533. # show queue position
  534. show()
  535. onQueue: (data) =>
  536. @log.notice 'onQueue', data.position
  537. @inQueue = true
  538. @el.find('.zammad-chat-modal').html @view('waiting')
  539. position: data.position
  540. onAgentTypingStart: =>
  541. if @stopTypingId
  542. clearTimeout(@stopTypingId)
  543. @stopTypingId = setTimeout(@onAgentTypingEnd, 3000)
  544. # never display two typing indicators
  545. return if @el.find('.zammad-chat-message--typing').size()
  546. @maybeAddTimestamp()
  547. @el.find('.zammad-chat-body').append @view('typingIndicator')()
  548. # only if typing indicator is shown
  549. return if !@isVisible(@el.find('.zammad-chat-message--typing'), true)
  550. @scrollToBottom()
  551. onAgentTypingEnd: =>
  552. @el.find('.zammad-chat-message--typing').remove()
  553. onLeaveTemporary: =>
  554. return if !@sessionId
  555. @send 'chat_session_leave_temporary',
  556. session_id: @sessionId
  557. maybeAddTimestamp: ->
  558. timestamp = Date.now()
  559. if !@lastTimestamp or (timestamp - @lastTimestamp) > @showTimeEveryXMinutes * 60000
  560. label = @T('Today')
  561. time = new Date().toTimeString().substr 0,5
  562. if @lastAddedType is 'timestamp'
  563. # update last time
  564. @updateLastTimestamp label, time
  565. @lastTimestamp = timestamp
  566. else
  567. # add new timestamp
  568. @el.find('.zammad-chat-body').append @view('timestamp')
  569. label: label
  570. time: time
  571. @lastTimestamp = timestamp
  572. @lastAddedType = 'timestamp'
  573. @scrollToBottom()
  574. updateLastTimestamp: (label, time) ->
  575. return if !@el
  576. @el.find('.zammad-chat-body')
  577. .find('.zammad-chat-timestamp')
  578. .last()
  579. .replaceWith @view('timestamp')
  580. label: label
  581. time: time
  582. addStatus: (status) ->
  583. return if !@el
  584. @maybeAddTimestamp()
  585. @el.find('.zammad-chat-body').append @view('status')
  586. status: status
  587. @scrollToBottom()
  588. detectScrolledtoBottom: =>
  589. scrollBottom = @el.find('.zammad-chat-body').scrollTop() + @el.find('.zammad-chat-body').outerHeight()
  590. @scrolledToBottom = Math.abs(scrollBottom - @el.find('.zammad-chat-body').prop('scrollHeight')) <= @scrollSnapTolerance
  591. @el.find('.zammad-scroll-hint').addClass('is-hidden') if @scrolledToBottom
  592. showScrollHint: ->
  593. @el.find('.zammad-scroll-hint').removeClass('is-hidden')
  594. # compensate scroll
  595. @el.find('.zammad-chat-body').scrollTop(@el.find('.zammad-chat-body').scrollTop() + @el.find('.zammad-scroll-hint').outerHeight())
  596. onScrollHintClick: =>
  597. # animate scroll
  598. @el.find('.zammad-chat-body').animate({scrollTop: @el.find('.zammad-chat-body').prop('scrollHeight')}, 300)
  599. scrollToBottom: ({ showHint } = { showHint: false }) ->
  600. if @scrolledToBottom
  601. @el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'))
  602. else if showHint
  603. @showScrollHint()
  604. destroy: (params = {}) =>
  605. @log.debug 'destroy widget', params
  606. @setAgentOnlineState 'offline'
  607. if params.remove && @el
  608. @el.remove()
  609. # stop all timer
  610. if @waitingListTimeout
  611. @waitingListTimeout.stop()
  612. if @inactiveTimeout
  613. @inactiveTimeout.stop()
  614. if @idleTimeout
  615. @idleTimeout.stop()
  616. # stop ws connection
  617. @io.close()
  618. reconnect: =>
  619. # set status to connecting
  620. @log.notice 'reconnecting'
  621. @disableInput()
  622. @lastAddedType = 'status'
  623. @setAgentOnlineState 'connecting'
  624. @addStatus @T('Connection lost')
  625. onConnectionReestablished: =>
  626. # set status back to online
  627. @lastAddedType = 'status'
  628. @setAgentOnlineState 'online'
  629. @addStatus @T('Connection re-established')
  630. onSessionClosed: (data) ->
  631. @addStatus @T('Chat closed by %s', data.realname)
  632. @disableInput()
  633. @setAgentOnlineState 'offline'
  634. @inactiveTimeout.stop()
  635. setSessionId: (id) =>
  636. @sessionId = id
  637. if id is undefined
  638. sessionStorage.removeItem 'sessionId'
  639. else
  640. sessionStorage.setItem 'sessionId', id
  641. onConnectionEstablished: (data) =>
  642. # stop delay of initial queue position
  643. if @onInitialQueueDelayId
  644. clearTimeout @onInitialQueueDelayId
  645. @inQueue = false
  646. if data.agent
  647. @agent = data.agent
  648. if data.session_id
  649. @setSessionId data.session_id
  650. # empty old messages
  651. @el.find('.zammad-chat-body').html('')
  652. @el.find('.zammad-chat-agent').html @view('agent')
  653. agent: @agent
  654. @enableInput()
  655. @hideModal()
  656. @el.find('.zammad-chat-welcome').addClass('zammad-chat-is-hidden')
  657. @el.find('.zammad-chat-agent').removeClass('zammad-chat-is-hidden')
  658. @el.find('.zammad-chat-agent-status').removeClass('zammad-chat-is-hidden')
  659. @input.focus() if not @isFullscreen
  660. @setAgentOnlineState 'online'
  661. @waitingListTimeout.stop()
  662. @idleTimeout.stop()
  663. @inactiveTimeout.start()
  664. showCustomerTimeout: ->
  665. @el.find('.zammad-chat-modal').html @view('customer_timeout')
  666. agent: @agent.name
  667. delay: @options.inactiveTimeout
  668. reload = ->
  669. location.reload()
  670. @el.find('.js-restart').click reload
  671. @sessionClose()
  672. showWaitingListTimeout: ->
  673. @el.find('.zammad-chat-modal').html @view('waiting_list_timeout')
  674. delay: @options.watingListTimeout
  675. reload = ->
  676. location.reload()
  677. @el.find('.js-restart').click reload
  678. @sessionClose()
  679. showLoader: ->
  680. @el.find('.zammad-chat-modal').html @view('loader')()
  681. setAgentOnlineState: (state) =>
  682. @state = state
  683. return if !@el
  684. capitalizedState = state.charAt(0).toUpperCase() + state.slice(1)
  685. @el
  686. .find('.zammad-chat-agent-status')
  687. .attr('data-status', state)
  688. .text @T(capitalizedState)
  689. detectHost: ->
  690. protocol = 'ws://'
  691. if window.location.protocol is 'https:'
  692. protocol = 'wss://'
  693. @options.host = "#{ protocol }#{ scriptHost }/ws"
  694. loadCss: ->
  695. return if !@options.cssAutoload
  696. url = @options.cssUrl
  697. if !url
  698. url = @options.host
  699. .replace(/^wss/i, 'https')
  700. .replace(/^ws/i, 'http')
  701. .replace(/\/ws/i, '')
  702. url += '/assets/chat/chat.css'
  703. @log.debug "load css from '#{url}'"
  704. styles = "@import url('#{url}');"
  705. newSS = document.createElement('link')
  706. newSS.onload = @onCssLoaded
  707. newSS.rel = 'stylesheet'
  708. newSS.href = 'data:text/css,' + escape(styles)
  709. document.getElementsByTagName('head')[0].appendChild(newSS)
  710. onCssLoaded: =>
  711. if @socketReady
  712. @onReady()
  713. else
  714. @cssLoaded = true
  715. startTimeoutObservers: =>
  716. @idleTimeout = new Timeout(
  717. logPrefix: 'idleTimeout'
  718. debug: @options.debug
  719. timeout: @options.idleTimeout
  720. timeoutIntervallCheck: @options.idleTimeoutIntervallCheck
  721. callback: =>
  722. @log.debug 'Idle timeout reached, hide widget', new Date
  723. @destroy(remove: true)
  724. )
  725. @inactiveTimeout = new Timeout(
  726. logPrefix: 'inactiveTimeout'
  727. debug: @options.debug
  728. timeout: @options.inactiveTimeout
  729. timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck
  730. callback: =>
  731. @log.debug 'Inactive timeout reached, show timeout screen.', new Date
  732. @showCustomerTimeout()
  733. @destroy(remove: false)
  734. )
  735. @waitingListTimeout = new Timeout(
  736. logPrefix: 'waitingListTimeout'
  737. debug: @options.debug
  738. timeout: @options.waitingListTimeout
  739. timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck
  740. callback: =>
  741. @log.debug 'Waiting list timeout reached, show timeout screen.', new Date
  742. @showWaitingListTimeout()
  743. @destroy(remove: false)
  744. )
  745. disableScrollOnRoot: ->
  746. @rootScrollOffset = @scrollRoot.scrollTop()
  747. @scrollRoot.css
  748. overflow: 'hidden'
  749. position: 'fixed'
  750. enableScrollOnRoot: ->
  751. @scrollRoot.scrollTop @rootScrollOffset
  752. @scrollRoot.css
  753. overflow: ''
  754. position: ''
  755. # based on https://github.com/customd/jquery-visible/blob/master/jquery.visible.js
  756. # to have not dependency, port to coffeescript
  757. isVisible: (el, partial, hidden, direction) ->
  758. return if el.length < 1
  759. $w = $(window)
  760. $t = if el.length > 1 then el.eq(0) else el
  761. t = $t.get(0)
  762. vpWidth = $w.width()
  763. vpHeight = $w.height()
  764. direction = if direction then direction else 'both'
  765. clientSize = if hidden is true then t.offsetWidth * t.offsetHeight else true
  766. if typeof t.getBoundingClientRect is 'function'
  767. # Use this native browser method, if available.
  768. rec = t.getBoundingClientRect()
  769. tViz = rec.top >= 0 && rec.top < vpHeight
  770. bViz = rec.bottom > 0 && rec.bottom <= vpHeight
  771. lViz = rec.left >= 0 && rec.left < vpWidth
  772. rViz = rec.right > 0 && rec.right <= vpWidth
  773. vVisible = if partial then tViz || bViz else tViz && bViz
  774. hVisible = if partial then lViz || rViz else lViz && rViz
  775. if direction is 'both'
  776. return clientSize && vVisible && hVisible
  777. else if direction is 'vertical'
  778. return clientSize && vVisible
  779. else if direction is 'horizontal'
  780. return clientSize && hVisible
  781. else
  782. viewTop = $w.scrollTop()
  783. viewBottom = viewTop + vpHeight
  784. viewLeft = $w.scrollLeft()
  785. viewRight = viewLeft + vpWidth
  786. offset = $t.offset()
  787. _top = offset.top
  788. _bottom = _top + $t.height()
  789. _left = offset.left
  790. _right = _left + $t.width()
  791. compareTop = if partial is true then _bottom else _top
  792. compareBottom = if partial is true then _top else _bottom
  793. compareLeft = if partial is true then _right else _left
  794. compareRight = if partial is true then _left else _right
  795. if direction is 'both'
  796. return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop)) && ((compareRight <= viewRight) && (compareLeft >= viewLeft))
  797. else if direction is 'vertical'
  798. return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop))
  799. else if direction is 'horizontal'
  800. return !!clientSize && ((compareRight <= viewRight) && (compareLeft >= viewLeft))
  801. window.ZammadChat = ZammadChat