First version of customer chat.
This commit is contained in:
parent
92264a55e2
commit
791c0f934c
19 changed files with 1256 additions and 159 deletions
383
app/assets/javascripts/app/controllers/chat.coffee
Normal file
383
app/assets/javascripts/app/controllers/chat.coffee
Normal file
|
@ -0,0 +1,383 @@
|
||||||
|
class App.CustomerChat extends App.Controller
|
||||||
|
@extend Spine.Events
|
||||||
|
|
||||||
|
events:
|
||||||
|
'click .js-acceptChat': 'acceptChat'
|
||||||
|
|
||||||
|
sounds:
|
||||||
|
chat_new: new Audio('assets/sounds/chat_new.mp3')
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@i = 0
|
||||||
|
@chatWindows = {}
|
||||||
|
@totalQuestions = 7
|
||||||
|
@answered = 0
|
||||||
|
@correct = 0
|
||||||
|
@wrong = 0
|
||||||
|
@maxChats = 4
|
||||||
|
|
||||||
|
@messageCounter = 0
|
||||||
|
@meta =
|
||||||
|
active: false
|
||||||
|
waiting_chat_count: 0
|
||||||
|
running_chat_count: 0
|
||||||
|
active_agents: 0
|
||||||
|
|
||||||
|
@render()
|
||||||
|
|
||||||
|
App.Event.bind(
|
||||||
|
'chat_status_agent'
|
||||||
|
(data) =>
|
||||||
|
@meta = data
|
||||||
|
@updateMeta()
|
||||||
|
)
|
||||||
|
App.Event.bind(
|
||||||
|
'chat_session_start'
|
||||||
|
(data) =>
|
||||||
|
App.WebSocket.send(event:'chat_status_agent')
|
||||||
|
if data.session
|
||||||
|
@addChat(data.session)
|
||||||
|
)
|
||||||
|
|
||||||
|
App.WebSocket.send(event:'chat_status_agent')
|
||||||
|
|
||||||
|
@interval(@pushState, 16000)
|
||||||
|
|
||||||
|
pushState: =>
|
||||||
|
App.WebSocket.send(
|
||||||
|
event:'chat_agent_state'
|
||||||
|
data:
|
||||||
|
active: @meta.active
|
||||||
|
)
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('customer_chat/index')()
|
||||||
|
|
||||||
|
show: (params) =>
|
||||||
|
@navupdate '#customer_chat'
|
||||||
|
|
||||||
|
counter: =>
|
||||||
|
if @meta.waiting_chat_count
|
||||||
|
if @waitingChatCountLast isnt @meta.waiting_chat_count
|
||||||
|
@sounds.chat_new.play()
|
||||||
|
@waitingChatCountLast = @meta.waiting_chat_count
|
||||||
|
|
||||||
|
return @messageCounter + @meta.waiting_chat_count
|
||||||
|
@messageCounter
|
||||||
|
|
||||||
|
switch: (state = undefined) =>
|
||||||
|
|
||||||
|
# read state
|
||||||
|
if state is undefined
|
||||||
|
return @meta.active
|
||||||
|
|
||||||
|
@meta.active = state
|
||||||
|
|
||||||
|
# write state
|
||||||
|
App.WebSocket.send(
|
||||||
|
event:'chat_agent_state'
|
||||||
|
data:
|
||||||
|
active: @meta.active
|
||||||
|
)
|
||||||
|
|
||||||
|
updateNavMenu: =>
|
||||||
|
delay = ->
|
||||||
|
App.Event.trigger('menu:render')
|
||||||
|
@delay(delay, 200)
|
||||||
|
|
||||||
|
updateMeta: =>
|
||||||
|
if @meta.waiting_chat_count
|
||||||
|
@$('.js-acceptChat').addClass('is-clickable is-blinking')
|
||||||
|
else
|
||||||
|
@$('.js-acceptChat').removeClass('is-clickable is-blinking')
|
||||||
|
@$('.js-badgeWaitingCustomers').text(@meta.waiting_chat_count)
|
||||||
|
@$('.js-badgeChattingCustomers').text(@meta.running_chat_count)
|
||||||
|
@$('.js-badgeActiveAgents').text(@meta.active_agents)
|
||||||
|
|
||||||
|
if @meta.active_sessions
|
||||||
|
for session in @meta.active_sessions
|
||||||
|
@addChat(session)
|
||||||
|
|
||||||
|
@updateNavMenu()
|
||||||
|
|
||||||
|
addChat: (session) ->
|
||||||
|
return if @chatWindows[session.session_id]
|
||||||
|
chat = new chatWindow
|
||||||
|
name: "#{session.created_at}"
|
||||||
|
session: session
|
||||||
|
callback: @removeChat
|
||||||
|
|
||||||
|
@on 'layout-has-changed', @propagateLayoutChange
|
||||||
|
|
||||||
|
@$('.chat-workspace').append(chat.el)
|
||||||
|
@chatWindows[session.session_id] = chat
|
||||||
|
|
||||||
|
removeChat: (session_id) =>
|
||||||
|
console.log('removeChat', session_id, @chatWindows[session_id])
|
||||||
|
delete @chatWindows[session_id]
|
||||||
|
|
||||||
|
propagateLayoutChange: (event) =>
|
||||||
|
|
||||||
|
# adjust scroll position on layoutChange
|
||||||
|
for session_id, chat of @chatWindows
|
||||||
|
chat.trigger 'layout-changed'
|
||||||
|
|
||||||
|
acceptChat: =>
|
||||||
|
currentChats = 0
|
||||||
|
for key, value of @chatWindows
|
||||||
|
if @chatWindows[key]
|
||||||
|
currentChats += 1
|
||||||
|
return if currentChats >= @maxChats
|
||||||
|
|
||||||
|
App.WebSocket.send(event:'chat_session_start')
|
||||||
|
|
||||||
|
class CustomerChatRouter extends App.ControllerPermanent
|
||||||
|
constructor: (params) ->
|
||||||
|
super
|
||||||
|
|
||||||
|
# check authentication
|
||||||
|
return if !@authenticate()
|
||||||
|
|
||||||
|
App.TaskManager.execute(
|
||||||
|
key: 'CustomerChat'
|
||||||
|
controller: 'CustomerChat'
|
||||||
|
params: {}
|
||||||
|
show: true
|
||||||
|
persistent: true
|
||||||
|
)
|
||||||
|
|
||||||
|
class chatWindow extends App.Controller
|
||||||
|
@extend Spine.Events
|
||||||
|
|
||||||
|
className: 'chat-window'
|
||||||
|
|
||||||
|
events:
|
||||||
|
'keydown .js-customerChatInput': 'onKeydown'
|
||||||
|
'focus .js-customerChatInput': 'clearUnread'
|
||||||
|
'click': 'clearUnread'
|
||||||
|
'click .js-send': 'sendMessage'
|
||||||
|
'click .js-close': 'close'
|
||||||
|
|
||||||
|
elements:
|
||||||
|
'.js-customerChatInput': 'input'
|
||||||
|
'.js-status': 'status'
|
||||||
|
'.js-body': 'body'
|
||||||
|
'.js-scrollHolder': 'scrollHolder'
|
||||||
|
|
||||||
|
sounds:
|
||||||
|
message: new Audio('assets/sounds/chat_message.mp3')
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
|
||||||
|
@showTimeEveryXMinutes = 1
|
||||||
|
@lastTimestamp
|
||||||
|
@lastAddedType
|
||||||
|
@isTyping = false
|
||||||
|
@render()
|
||||||
|
|
||||||
|
@on 'layout-change', @scrollToBottom
|
||||||
|
|
||||||
|
App.Event.bind(
|
||||||
|
'chat_session_typing'
|
||||||
|
(data) =>
|
||||||
|
return if data.session_id isnt @session.session_id
|
||||||
|
return if data.self_written
|
||||||
|
@showWritingLoader()
|
||||||
|
)
|
||||||
|
App.Event.bind(
|
||||||
|
'chat_session_message'
|
||||||
|
(data) =>
|
||||||
|
return if data.session_id isnt @session.session_id
|
||||||
|
return if data.self_written
|
||||||
|
@receiveMessage(data.message.content)
|
||||||
|
)
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html App.view('layout_ref/customer_chat_window')
|
||||||
|
name: @options.name
|
||||||
|
|
||||||
|
@el.one 'transitionend', @onTransitionend
|
||||||
|
|
||||||
|
# make sure animation will run
|
||||||
|
setTimeout (=> @el.addClass('is-open')), 0
|
||||||
|
|
||||||
|
# @addMessage 'Hello. My name is Roger, how can I help you?', 'agent'
|
||||||
|
if @session && @session.messages
|
||||||
|
for message in @session.messages
|
||||||
|
if message.created_by_id
|
||||||
|
@addMessage message.content, 'agent'
|
||||||
|
else
|
||||||
|
@addMessage message.content, 'customer'
|
||||||
|
|
||||||
|
# set focus
|
||||||
|
@input.get(0).focus()
|
||||||
|
|
||||||
|
onTransitionend: (event) =>
|
||||||
|
# chat window is done with animation - adjust scroll-bars
|
||||||
|
# of sibling chat windows
|
||||||
|
@trigger 'layout-has-changed'
|
||||||
|
|
||||||
|
if event.data and event.data.callback
|
||||||
|
event.data.callback()
|
||||||
|
|
||||||
|
close: =>
|
||||||
|
@el.one 'transitionend', { callback: @release }, @onTransitionend
|
||||||
|
@el.removeClass('is-open')
|
||||||
|
App.WebSocket.send(
|
||||||
|
event:'chat_session_close'
|
||||||
|
data:
|
||||||
|
session_id: @session.session_id
|
||||||
|
)
|
||||||
|
if @callback
|
||||||
|
@callback(@session.session_id)
|
||||||
|
|
||||||
|
release: =>
|
||||||
|
@trigger 'closed'
|
||||||
|
super
|
||||||
|
|
||||||
|
clearUnread: =>
|
||||||
|
@$('.chat-message--new').removeClass('chat-message--new')
|
||||||
|
@updateModified(false)
|
||||||
|
|
||||||
|
onKeydown: (event) =>
|
||||||
|
TABKEY = 9;
|
||||||
|
ENTERKEY = 13;
|
||||||
|
|
||||||
|
if event.keyCode isnt TABKEY && event.keyCode isnt ENTERKEY
|
||||||
|
App.WebSocket.send(
|
||||||
|
event:'chat_session_typing'
|
||||||
|
data:
|
||||||
|
session_id: @session.session_id
|
||||||
|
)
|
||||||
|
|
||||||
|
switch event.keyCode
|
||||||
|
when TABKEY
|
||||||
|
allChatInputs = $('.js-customerChatInput').not('[disabled="disabled"]')
|
||||||
|
chatCount = allChatInputs.size()
|
||||||
|
index = allChatInputs.index(@input)
|
||||||
|
|
||||||
|
if chatCount > 1
|
||||||
|
switch index
|
||||||
|
when chatCount-1
|
||||||
|
if !event.shiftKey
|
||||||
|
# State: tab without shift on last input
|
||||||
|
# Jump to first input
|
||||||
|
event.preventDefault()
|
||||||
|
allChatInputs.eq(0).focus()
|
||||||
|
when 0
|
||||||
|
if event.shiftKey
|
||||||
|
# State: tab with shift on first input
|
||||||
|
# Jump to last input
|
||||||
|
event.preventDefault()
|
||||||
|
allChatInputs.eq(chatCount-1).focus()
|
||||||
|
|
||||||
|
when ENTERKEY
|
||||||
|
if !event.shiftKey
|
||||||
|
event.preventDefault()
|
||||||
|
@sendMessage()
|
||||||
|
|
||||||
|
sendMessage: =>
|
||||||
|
content = @input.html()
|
||||||
|
return if !content
|
||||||
|
|
||||||
|
#@trigger "answer", @input.html()
|
||||||
|
App.WebSocket.send(
|
||||||
|
event:'chat_session_message'
|
||||||
|
data:
|
||||||
|
content: content
|
||||||
|
session_id: @session.session_id
|
||||||
|
)
|
||||||
|
|
||||||
|
@addMessage content, 'agent'
|
||||||
|
@input.html('')
|
||||||
|
|
||||||
|
updateModified: (state) =>
|
||||||
|
@status.toggleClass('is-modified', state)
|
||||||
|
|
||||||
|
receiveMessage: (message) =>
|
||||||
|
isFocused = @input.is(':focus')
|
||||||
|
|
||||||
|
@removeWritingLoader()
|
||||||
|
@addMessage(message, 'customer', !isFocused)
|
||||||
|
|
||||||
|
if !isFocused
|
||||||
|
@updateModified(true)
|
||||||
|
@sounds.message.play()
|
||||||
|
|
||||||
|
addMessage: (message, sender, isNew) =>
|
||||||
|
@maybeAddTimestamp()
|
||||||
|
|
||||||
|
@lastAddedType = sender
|
||||||
|
|
||||||
|
@body.append App.view('layout_ref/customer_chat_message')
|
||||||
|
message: message
|
||||||
|
sender: sender
|
||||||
|
isNew: isNew
|
||||||
|
timestamp: Date.now()
|
||||||
|
|
||||||
|
@scrollToBottom()
|
||||||
|
|
||||||
|
showWritingLoader: =>
|
||||||
|
if !@isTyping
|
||||||
|
@isTyping = true
|
||||||
|
@maybeAddTimestamp()
|
||||||
|
@body.append App.view('layout_ref/customer_chat_loader')()
|
||||||
|
@scrollToBottom()
|
||||||
|
|
||||||
|
# clear old delay, set new
|
||||||
|
@delay(@removeWritingLoader, 1800, 'typing')
|
||||||
|
|
||||||
|
removeWritingLoader: =>
|
||||||
|
@isTyping = false
|
||||||
|
@$('.js-loader').remove()
|
||||||
|
|
||||||
|
goOffline: =>
|
||||||
|
@addStatusMessage("<strong>#{ @options.name }</strong>'s connection got closed")
|
||||||
|
@status.attr('data-status', 'offline')
|
||||||
|
@el.addClass('is-offline')
|
||||||
|
@input.attr('disabled', true)
|
||||||
|
|
||||||
|
maybeAddTimestamp: ->
|
||||||
|
timestamp = Date.now()
|
||||||
|
|
||||||
|
if !@lastTimestamp or timestamp - @lastTimestamp > @showTimeEveryXMinutes * 60000
|
||||||
|
label = 'Today'
|
||||||
|
time = new Date().toTimeString().substr(0,5)
|
||||||
|
if @lastAddedType is 'timestamp'
|
||||||
|
# update last time
|
||||||
|
@updateLastTimestamp label, time
|
||||||
|
@lastTimestamp = timestamp
|
||||||
|
else
|
||||||
|
@addTimestamp label, time
|
||||||
|
@lastTimestamp = timestamp
|
||||||
|
@lastAddedType = 'timestamp'
|
||||||
|
|
||||||
|
addTimestamp: (label, time) =>
|
||||||
|
@body.append App.view('layout_ref/customer_chat_timestamp')
|
||||||
|
label: label
|
||||||
|
time: time
|
||||||
|
|
||||||
|
updateLastTimestamp: (label, time) ->
|
||||||
|
@body
|
||||||
|
.find('.js-timestamp')
|
||||||
|
.last()
|
||||||
|
.replaceWith App.view('layout_ref/customer_chat_timestamp')
|
||||||
|
label: label
|
||||||
|
time: time
|
||||||
|
|
||||||
|
addStatusMessage: (message) ->
|
||||||
|
@body.append App.view('layout_ref/customer_chat_status_message')
|
||||||
|
message: message
|
||||||
|
|
||||||
|
@scrollToBottom()
|
||||||
|
|
||||||
|
scrollToBottom: ->
|
||||||
|
@scrollHolder.scrollTop(@scrollHolder.prop('scrollHeight'))
|
||||||
|
|
||||||
|
|
||||||
|
App.Config.set( 'customer_chat', CustomerChatRouter, 'Routes' )
|
||||||
|
App.Config.set( 'CustomerChat', { controller: 'CustomerChat', authentication: true }, 'permanentTask' )
|
||||||
|
App.Config.set( 'CustomerChat', { prio: 1200, parent: '', name: 'Customer Chat', target: '#customer_chat', key: 'CustomerChat', role: ['Chat'], class: 'chat' }, 'NavBar' )
|
24
app/assets/javascripts/app/views/customer_chat/index.jst.eco
Normal file
24
app/assets/javascripts/app/views/customer_chat/index.jst.eco
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<div class="chat main">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-header-title">
|
||||||
|
<h1><%- @T('Customer Chat') %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-center">
|
||||||
|
<div class="status-fields">
|
||||||
|
<div class="status-field js-acceptChat">
|
||||||
|
<span class="badge js-badgeWaitingCustomers"></span> <%- @T('Waiting Customers') %>
|
||||||
|
</div>
|
||||||
|
<div class="status-field">
|
||||||
|
<span class="badge js-badgeChattingCustomers"></span> <%- @T('Chatting Customers') %>
|
||||||
|
</div>
|
||||||
|
<div class="status-field">
|
||||||
|
<span class="badge js-badgeActiveAgents"></span> <%- @T('Active Agents') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-header-meta">
|
||||||
|
<div class="btn btn--action" data-type="settings"><%= @T('Settings') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chat-workspace"></div>
|
||||||
|
</div>
|
|
@ -26,7 +26,7 @@
|
||||||
<span class="menu-item-name">
|
<span class="menu-item-name">
|
||||||
<%- @T(item.name) %>
|
<%- @T(item.name) %>
|
||||||
</span>
|
</span>
|
||||||
<% if item.counter isnt undefined: %>
|
<% if item.counter isnt undefined && item.counter isnt 0: %>
|
||||||
<span class="counter badge badge--big"><%= item.counter %></span>
|
<span class="counter badge badge--big"><%= item.counter %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if item.switch isnt undefined: %>
|
<% if item.switch isnt undefined: %>
|
||||||
|
|
133
app/models/chat.rb
Normal file
133
app/models/chat.rb
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Chat < ApplicationModel
|
||||||
|
has_many :chat_topics
|
||||||
|
validates :name, presence: true
|
||||||
|
|
||||||
|
def state
|
||||||
|
return { state: 'chat_disabled' } if !Setting.get('chat')
|
||||||
|
|
||||||
|
if Chat::Agent.where(active: true).where('updated_at > ?', Time.zone.now - 2.minutes).count > 0
|
||||||
|
if active_chat_count >= max_queue
|
||||||
|
return {
|
||||||
|
state: 'no_seats_available',
|
||||||
|
queue: seads_available,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return { state: 'online' }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{ state: 'offline' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.agent_state(user_id)
|
||||||
|
return { state: 'chat_disabled' } if !Setting.get('chat')
|
||||||
|
actice_sessions = []
|
||||||
|
Chat::Session.where(state: 'running').order('created_at ASC').each {|session|
|
||||||
|
session_attributes = session.attributes
|
||||||
|
session_attributes['messages'] = []
|
||||||
|
Chat::Message.where(chat_session_id: session.id).each { |message|
|
||||||
|
session_attributes['messages'].push message.attributes
|
||||||
|
}
|
||||||
|
actice_sessions.push session_attributes
|
||||||
|
}
|
||||||
|
{
|
||||||
|
waiting_chat_count: waiting_chat_count,
|
||||||
|
running_chat_count: running_chat_count,
|
||||||
|
#available_agents: available_agents,
|
||||||
|
active_sessions: actice_sessions,
|
||||||
|
active: Chat::Agent.state(user_id)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.waiting_chat_count
|
||||||
|
Chat::Session.where(state: ['waiting']).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.running_chat_count
|
||||||
|
Chat::Session.where(state: ['waiting']).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def active_chat_count
|
||||||
|
Chat::Session.where(state: %w(waiting running)).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def available_agents(diff = 2.minutes)
|
||||||
|
agents = {}
|
||||||
|
Chat::Agent.where(active: true).where('updated_at > ?', Time.zone.now - diff).each {|record|
|
||||||
|
agents[record.updated_by_id] = record.concurrent
|
||||||
|
}
|
||||||
|
agents
|
||||||
|
end
|
||||||
|
|
||||||
|
def seads_total(diff = 2.minutes)
|
||||||
|
total = 0
|
||||||
|
available_agents(diff).each {|_record, concurrent|
|
||||||
|
total += concurrent
|
||||||
|
}
|
||||||
|
total
|
||||||
|
end
|
||||||
|
|
||||||
|
def seads_available(diff = 2.minutes)
|
||||||
|
seads_total(diff) - active_chat_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Chat::Topic < ApplicationModel
|
||||||
|
end
|
||||||
|
|
||||||
|
class Chat::Agent < ApplicationModel
|
||||||
|
|
||||||
|
def seads_available
|
||||||
|
concurrent - active_chat_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def active_chat_count
|
||||||
|
Chat::Session.where(state: %w(waiting running), user_id: updated_by_id).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.state(user_id, state = nil)
|
||||||
|
chat_agent = Chat::Agent.find_by(
|
||||||
|
updated_by_id: user_id
|
||||||
|
)
|
||||||
|
if state.nil?
|
||||||
|
return false if !chat_agent
|
||||||
|
return chat_agent.active
|
||||||
|
end
|
||||||
|
if chat_agent
|
||||||
|
chat_agent.active = state
|
||||||
|
chat_agent.updated_at = Time.zone.now
|
||||||
|
chat_agent.save
|
||||||
|
else
|
||||||
|
Chat::Agent.create(
|
||||||
|
active: state,
|
||||||
|
updated_by_id: user_id,
|
||||||
|
created_by_id: user_id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create_or_update(params)
|
||||||
|
chat_agent = Chat::Agent.find_by(
|
||||||
|
updated_by_id: params[:updated_by_id]
|
||||||
|
)
|
||||||
|
if chat_agent
|
||||||
|
chat_agent.update_attributes(params)
|
||||||
|
else
|
||||||
|
Chat::Agent.create(params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Chat::Session < ApplicationModel
|
||||||
|
before_create :generate_session_id
|
||||||
|
store :preferences
|
||||||
|
|
||||||
|
def generate_session_id
|
||||||
|
self.session_id = Digest::MD5.hexdigest(Time.zone.now.to_s + rand(99_999_999_999_999).to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Chat::Message < ApplicationModel
|
||||||
|
end
|
93
db/migrate/20151109000001_create_chat.rb
Normal file
93
db/migrate/20151109000001_create_chat.rb
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
class CreateChat < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
=begin
|
||||||
|
ActiveRecord::Migration.drop_table :chats
|
||||||
|
ActiveRecord::Migration.drop_table :chat_topics
|
||||||
|
ActiveRecord::Migration.drop_table :chat_sessions
|
||||||
|
ActiveRecord::Migration.drop_table :chat_messages
|
||||||
|
ActiveRecord::Migration.drop_table :chat_agents
|
||||||
|
=end
|
||||||
|
create_table :chats do |t|
|
||||||
|
t.string :name, limit: 250, null: true
|
||||||
|
t.integer :max_queue, null: false, default: 5
|
||||||
|
t.string :note, limit: 250, null: true
|
||||||
|
t.boolean :active, null: false, default: true
|
||||||
|
t.integer :updated_by_id, null: false
|
||||||
|
t.integer :created_by_id, null: false
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
add_index :chats, [:name], unique: true
|
||||||
|
|
||||||
|
create_table :chat_topics do |t|
|
||||||
|
t.integer :chat_id, null: false
|
||||||
|
t.string :name, limit: 250, null: false
|
||||||
|
t.string :note, limit: 250, null: true
|
||||||
|
t.integer :updated_by_id, null: false
|
||||||
|
t.integer :created_by_id, null: false
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
add_index :chat_topics, [:name], unique: true
|
||||||
|
|
||||||
|
create_table :chat_sessions do |t|
|
||||||
|
t.integer :chat_id, null: false
|
||||||
|
t.string :session_id, null: false
|
||||||
|
t.string :name, limit: 250, null: true
|
||||||
|
t.string :state, limit: 50, null: false, default: 'waiting' # running, closed
|
||||||
|
t.integer :user_id, null: true
|
||||||
|
t.text :preferences, limit: 100.kilobytes + 1, null: true
|
||||||
|
t.integer :updated_by_id, null: true
|
||||||
|
t.integer :created_by_id, null: true
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
add_index :chat_sessions, [:state]
|
||||||
|
add_index :chat_sessions, [:user_id]
|
||||||
|
|
||||||
|
create_table :chat_messages do |t|
|
||||||
|
t.integer :chat_session_id, null: false
|
||||||
|
t.string :content, limit: 5000, null: false
|
||||||
|
t.integer :created_by_id, null: true
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :chat_agents do |t|
|
||||||
|
t.boolean :active, null: false, default: true
|
||||||
|
t.integer :concurrent, null: false, default: 5
|
||||||
|
t.integer :updated_by_id, null: false
|
||||||
|
t.integer :created_by_id, null: false
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
add_index :chat_agents, [:created_by_id], unique: true
|
||||||
|
|
||||||
|
Role.create_if_not_exists(
|
||||||
|
name: 'Chat',
|
||||||
|
note: 'Access to chat feature.',
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1
|
||||||
|
)
|
||||||
|
|
||||||
|
chat = Chat.create(
|
||||||
|
name: 'default',
|
||||||
|
max_queue: 5,
|
||||||
|
note: '',
|
||||||
|
active: true,
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_topic = Chat::Topic.create(
|
||||||
|
chat_id: chat.id,
|
||||||
|
name: 'default',
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :chat_topics
|
||||||
|
drop_table :chat_sessions
|
||||||
|
drop_table :chat_messages
|
||||||
|
drop_table :chat_agents
|
||||||
|
drop_table :chats
|
||||||
|
end
|
||||||
|
end
|
18
lib/sessions/event.rb
Normal file
18
lib/sessions/event.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
class Sessions::Event
|
||||||
|
include ApplicationLib
|
||||||
|
|
||||||
|
def self.run(event, data, session, client_id)
|
||||||
|
adapter = "Sessions::Event::#{event.to_classname}"
|
||||||
|
begin
|
||||||
|
backend = load_adapter(adapter)
|
||||||
|
rescue => e
|
||||||
|
return { error: "No such event #{event}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Base.establish_connection
|
||||||
|
result = backend.run(data, session, client_id)
|
||||||
|
ActiveRecord::Base.remove_connection
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
38
lib/sessions/event/chat_agent_state.rb
Normal file
38
lib/sessions/event/chat_agent_state.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
class Sessions::Event::ChatAgentState
|
||||||
|
|
||||||
|
def self.run(data, session, _client_id)
|
||||||
|
|
||||||
|
# check if feature is enabled
|
||||||
|
if !Setting.get('chat')
|
||||||
|
return {
|
||||||
|
event: 'chat_agent_state',
|
||||||
|
data: {
|
||||||
|
state: 'chat_disabled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# only agents can do this
|
||||||
|
chat_id = 1
|
||||||
|
chat = Chat.find_by(id: chat_id)
|
||||||
|
if !session['id']
|
||||||
|
return {
|
||||||
|
event: 'chat_agent_state',
|
||||||
|
data: {
|
||||||
|
state: 'failed',
|
||||||
|
message: 'No such user_id.'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
Chat::Agent.state(session['id'], data['data']['active'])
|
||||||
|
|
||||||
|
{
|
||||||
|
event: 'chat_agent_state',
|
||||||
|
data: {
|
||||||
|
state: 'ok',
|
||||||
|
active: data['data']['active'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
46
lib/sessions/event/chat_session_close.rb
Normal file
46
lib/sessions/event/chat_session_close.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
class Sessions::Event::ChatSessionClose
|
||||||
|
|
||||||
|
def self.run(data, _session, _client_id)
|
||||||
|
|
||||||
|
# check if feature is enabled
|
||||||
|
if !Setting.get('chat')
|
||||||
|
return {
|
||||||
|
event: 'chat_status_close',
|
||||||
|
data: {
|
||||||
|
state: 'chat_disabled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
if !data['data'] || !data['data']['session_id']
|
||||||
|
return {
|
||||||
|
event: 'chat_status_close',
|
||||||
|
data: {
|
||||||
|
state: 'Need session_id.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
chat_session = Chat::Session.find_by(session_id: data['data']['session_id'])
|
||||||
|
if !chat_session
|
||||||
|
return {
|
||||||
|
event: 'chat_status_close',
|
||||||
|
data: {
|
||||||
|
state: "No such session id #{data['data']['session_id']}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
chat_session.state = 'closed'
|
||||||
|
chat_session.save
|
||||||
|
|
||||||
|
# return new session
|
||||||
|
{
|
||||||
|
event: 'chat_status_close',
|
||||||
|
data: {
|
||||||
|
state: 'ok',
|
||||||
|
session_id: chat_session.session_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
55
lib/sessions/event/chat_session_init.rb
Normal file
55
lib/sessions/event/chat_session_init.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
class Sessions::Event::ChatSessionInit
|
||||||
|
|
||||||
|
def self.run(data, _session, client_id)
|
||||||
|
|
||||||
|
# check if feature is enabled
|
||||||
|
if !Setting.get('chat')
|
||||||
|
return {
|
||||||
|
event: 'chat_session_init',
|
||||||
|
data: {
|
||||||
|
state: 'chat_disabled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
chat_id = 1
|
||||||
|
chat = Chat.find_by(id: chat_id)
|
||||||
|
if !chat
|
||||||
|
return {
|
||||||
|
event: 'chat_session_init',
|
||||||
|
data: {
|
||||||
|
state: 'no_such_chat',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# create chat session
|
||||||
|
chat_session = Chat::Session.create(
|
||||||
|
chat_id: chat_id,
|
||||||
|
name: '',
|
||||||
|
state: 'waiting',
|
||||||
|
preferences: {
|
||||||
|
participants: [client_id],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# send update to agents
|
||||||
|
User.where(active: true).each {|user|
|
||||||
|
data = {
|
||||||
|
event: 'chat_status_agent',
|
||||||
|
data: Chat.agent_state(user.id),
|
||||||
|
}
|
||||||
|
Sessions.send_to(user.id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
# return new session
|
||||||
|
{
|
||||||
|
event: 'chat_session_init',
|
||||||
|
data: {
|
||||||
|
state: 'queue',
|
||||||
|
position: Chat.waiting_chat_count,
|
||||||
|
session_id: chat_session.session_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
68
lib/sessions/event/chat_session_message.rb
Normal file
68
lib/sessions/event/chat_session_message.rb
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
class Sessions::Event::ChatSessionMessage
|
||||||
|
|
||||||
|
def self.run(data, session, client_id)
|
||||||
|
|
||||||
|
# check if feature is enabled
|
||||||
|
if !Setting.get('chat')
|
||||||
|
return {
|
||||||
|
event: 'chat_session_message',
|
||||||
|
data: {
|
||||||
|
state: 'chat_disabled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
if !data['data'] || !data['data']['session_id']
|
||||||
|
return {
|
||||||
|
event: 'chat_session_message',
|
||||||
|
data: {
|
||||||
|
state: 'Need session_id.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
chat_session = Chat::Session.find_by(session_id: data['data']['session_id'])
|
||||||
|
if !chat_session
|
||||||
|
return {
|
||||||
|
event: 'chat_session_message',
|
||||||
|
data: {
|
||||||
|
state: "No such session id #{data['data']['session_id']}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
user_id = nil
|
||||||
|
if session
|
||||||
|
user_id = session['id']
|
||||||
|
end
|
||||||
|
chat_message = Chat::Message.create(
|
||||||
|
chat_session_id: chat_session.id,
|
||||||
|
content: data['data']['content'],
|
||||||
|
created_by_id: user_id,
|
||||||
|
)
|
||||||
|
message = {
|
||||||
|
event: 'chat_session_message',
|
||||||
|
data: {
|
||||||
|
session_id: chat_session.session_id,
|
||||||
|
message: chat_message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# send to participents
|
||||||
|
chat_session.preferences[:participants].each {|local_client_id|
|
||||||
|
next if local_client_id == client_id
|
||||||
|
Sessions.send(local_client_id, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
# send chat_session_init to agent
|
||||||
|
{
|
||||||
|
event: 'chat_session_message',
|
||||||
|
data: {
|
||||||
|
session_id: chat_session.session_id,
|
||||||
|
message: chat_message,
|
||||||
|
self_written: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
60
lib/sessions/event/chat_session_start.rb
Normal file
60
lib/sessions/event/chat_session_start.rb
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
class Sessions::Event::ChatSessionStart
|
||||||
|
|
||||||
|
def self.run(data, session, client_id)
|
||||||
|
|
||||||
|
# check if feature is enabled
|
||||||
|
if !Setting.get('chat')
|
||||||
|
return {
|
||||||
|
event: 'chat_session_start',
|
||||||
|
data: {
|
||||||
|
state: 'chat_disabled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# find first in waiting list
|
||||||
|
chat_session = Chat::Session.where(state: 'waiting').order('created_at ASC').first
|
||||||
|
if !chat_session
|
||||||
|
return {
|
||||||
|
event: 'chat_session_start',
|
||||||
|
data: {
|
||||||
|
state: 'failed',
|
||||||
|
message: 'No session available.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
chat_session.user_id = session['id']
|
||||||
|
chat_session.state = 'running'
|
||||||
|
chat_session.preferences[:participants].push client_id
|
||||||
|
chat_session.save
|
||||||
|
|
||||||
|
# send chat_session_init to client
|
||||||
|
chat_user = User.find(chat_session.user_id)
|
||||||
|
user = {
|
||||||
|
name: chat_user.fullname,
|
||||||
|
avatar: chat_user.image,
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
event: 'chat_session_start',
|
||||||
|
data: {
|
||||||
|
state: 'ok',
|
||||||
|
agent: user,
|
||||||
|
session_id: chat_session.session_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
chat_session.preferences[:participants].each {|local_client_id|
|
||||||
|
next if local_client_id == client_id
|
||||||
|
Sessions.send(local_client_id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
# send chat_session_init to agent
|
||||||
|
{
|
||||||
|
event: 'chat_session_start',
|
||||||
|
data: {
|
||||||
|
state: 'ok',
|
||||||
|
session: chat_session,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
61
lib/sessions/event/chat_session_typing.rb
Normal file
61
lib/sessions/event/chat_session_typing.rb
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
class Sessions::Event::ChatSessionTyping
|
||||||
|
|
||||||
|
def self.run(data, session, client_id)
|
||||||
|
|
||||||
|
# check if feature is enabled
|
||||||
|
if !Setting.get('chat')
|
||||||
|
return {
|
||||||
|
event: 'chat_session_typing',
|
||||||
|
data: {
|
||||||
|
state: 'chat_disabled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
if !data['data'] || !data['data']['session_id']
|
||||||
|
return {
|
||||||
|
event: 'chat_session_typing',
|
||||||
|
data: {
|
||||||
|
state: 'Need session_id.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
chat_session = Chat::Session.find_by(session_id: data['data']['session_id'])
|
||||||
|
if !chat_session
|
||||||
|
return {
|
||||||
|
event: 'chat_session_typing',
|
||||||
|
data: {
|
||||||
|
state: "No such session id #{data['data']['session_id']}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
user_id = nil
|
||||||
|
if session
|
||||||
|
user_id = session['id']
|
||||||
|
end
|
||||||
|
message = {
|
||||||
|
event: 'chat_session_typing',
|
||||||
|
data: {
|
||||||
|
session_id: chat_session.session_id,
|
||||||
|
user_id: user_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# send to participents
|
||||||
|
chat_session.preferences[:participants].each {|local_client_id|
|
||||||
|
next if local_client_id == client_id
|
||||||
|
Sessions.send(local_client_id, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
# send chat_session_init to agent
|
||||||
|
{
|
||||||
|
event: 'chat_session_typing',
|
||||||
|
data: {
|
||||||
|
session_id: chat_session.session_id,
|
||||||
|
self_written: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
31
lib/sessions/event/chat_status.rb
Normal file
31
lib/sessions/event/chat_status.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
class Sessions::Event::ChatStatus
|
||||||
|
|
||||||
|
def self.run(_data, _session, _client_id)
|
||||||
|
|
||||||
|
# check if feature is enabled
|
||||||
|
if !Setting.get('chat')
|
||||||
|
return {
|
||||||
|
event: 'chat_status',
|
||||||
|
data: {
|
||||||
|
state: 'chat_disabled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
chat_id = 1
|
||||||
|
chat = Chat.find_by(id: chat_id)
|
||||||
|
if !chat
|
||||||
|
return {
|
||||||
|
event: 'chat_status',
|
||||||
|
data: {
|
||||||
|
state: 'no_such_chat',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
event: 'chat_status',
|
||||||
|
data: chat.state,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
27
lib/sessions/event/chat_status_agent.rb
Normal file
27
lib/sessions/event/chat_status_agent.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
class Sessions::Event::ChatStatusAgent
|
||||||
|
|
||||||
|
def self.run(_data, session, _client_id)
|
||||||
|
|
||||||
|
# check if user has permissions
|
||||||
|
|
||||||
|
# check if feature is enabled
|
||||||
|
if !Setting.get('chat')
|
||||||
|
return {
|
||||||
|
event: 'chat_status_agent',
|
||||||
|
data: {
|
||||||
|
state: 'chat_disabled',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# renew timestamps
|
||||||
|
state = Chat::Agent.state(session['id'])
|
||||||
|
Chat::Agent.state(session['id'], state)
|
||||||
|
|
||||||
|
{
|
||||||
|
event: 'chat_status_agent',
|
||||||
|
data: Chat.agent_state(session['id']),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -51,6 +51,8 @@ do($ = window.jQuery, window) ->
|
||||||
input: @onInput
|
input: @onInput
|
||||||
).autoGrow { extraLine: false }
|
).autoGrow { extraLine: false }
|
||||||
|
|
||||||
|
@session_id = undefined
|
||||||
|
|
||||||
if !window.WebSocket
|
if !window.WebSocket
|
||||||
console.log('Zammad Chat: Browser not supported')
|
console.log('Zammad Chat: Browser not supported')
|
||||||
return
|
return
|
||||||
|
@ -61,7 +63,7 @@ do($ = window.jQuery, window) ->
|
||||||
|
|
||||||
@ws.onopen = =>
|
@ws.onopen = =>
|
||||||
console.log('ws connected')
|
console.log('ws connected')
|
||||||
@send "chat_status"
|
@send 'chat_status'
|
||||||
|
|
||||||
@ws.onmessage = @onWebSocketMessage
|
@ws.onmessage = @onWebSocketMessage
|
||||||
|
|
||||||
|
@ -76,40 +78,52 @@ do($ = window.jQuery, window) ->
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@sendMessage()
|
@sendMessage()
|
||||||
|
|
||||||
send: (action, data) =>
|
send: (event, data) =>
|
||||||
|
console.log 'debug', 'ws:send', event, data
|
||||||
pipe = JSON.stringify
|
pipe = JSON.stringify
|
||||||
action: action
|
event: event
|
||||||
data: data
|
data: data
|
||||||
|
|
||||||
@ws.send pipe
|
@ws.send pipe
|
||||||
|
|
||||||
onWebSocketMessage: (e) =>
|
onWebSocketMessage: (e) =>
|
||||||
pipe = JSON.parse( e.data )
|
pipes = JSON.parse( e.data )
|
||||||
console.log 'debug', 'ws:onmessage', pipe
|
console.log 'debug', 'ws:onmessage', pipes
|
||||||
|
|
||||||
switch pipe.action
|
for pipe in pipes
|
||||||
when 'chat_message'
|
switch pipe.event
|
||||||
@receiveMessage pipe.data
|
when 'chat_session_message'
|
||||||
when 'chat_typing_start'
|
return if pipe.data.self_written
|
||||||
@onAgentTypingStart()
|
@receiveMessage pipe.data
|
||||||
when 'chat_typing_end'
|
when 'chat_session_typing'
|
||||||
@onAgentTypingEnd()
|
return if pipe.data.self_written
|
||||||
when 'chat_init'
|
@onAgentTypingStart()
|
||||||
switch pipe.data.state
|
if @stopTypingId
|
||||||
when 'ok'
|
clearTimeout(@stopTypingId)
|
||||||
@onConnectionEstablished pipe.data.agent
|
delay = =>
|
||||||
when 'queue'
|
@onAgentTypingEnd()
|
||||||
@onQueue pipe.data.position
|
@stopTypingId = setTimeout(delay, 3000)
|
||||||
when 'chat_status'
|
when 'chat_session_start'
|
||||||
switch pipe.data.state
|
switch pipe.data.state
|
||||||
when 'ok'
|
when 'ok'
|
||||||
@onReady()
|
@onConnectionEstablished pipe.data.agent
|
||||||
when 'offline'
|
when 'chat_session_init'
|
||||||
console.log 'Zammad Chat: No agent online'
|
switch pipe.data.state
|
||||||
when 'chat_disabled'
|
when 'ok'
|
||||||
console.log 'Zammad Chat: Chat is disabled'
|
@onConnectionEstablished pipe.data.agent
|
||||||
when 'no_seats_available'
|
when 'queue'
|
||||||
console.log 'Zammad Chat: Too many clients in queue. Clients in queue: ', pipe.data.queue
|
@onQueue pipe.data.position
|
||||||
|
@session_id = pipe.data.session_id
|
||||||
|
when 'chat_status'
|
||||||
|
switch pipe.data.state
|
||||||
|
when 'online'
|
||||||
|
@onReady()
|
||||||
|
console.log 'Zammad Chat: ready'
|
||||||
|
when 'offline'
|
||||||
|
console.log 'Zammad Chat: No agent online'
|
||||||
|
when 'chat_disabled'
|
||||||
|
console.log 'Zammad Chat: Chat is disabled'
|
||||||
|
when 'no_seats_available'
|
||||||
|
console.log 'Zammad Chat: Too many clients in queue. Clients in queue: ', pipe.data.queue
|
||||||
|
|
||||||
onReady: =>
|
onReady: =>
|
||||||
@show() if @options.show
|
@show() if @options.show
|
||||||
|
@ -119,22 +133,22 @@ do($ = window.jQuery, window) ->
|
||||||
@el.find('.zammad-chat-message--unread')
|
@el.find('.zammad-chat-message--unread')
|
||||||
.removeClass 'zammad-chat-message--unread'
|
.removeClass 'zammad-chat-message--unread'
|
||||||
|
|
||||||
clearTimeout(@inputTimeout) if @inputTimeout
|
@onTypingStart()
|
||||||
|
|
||||||
# fire typingEnd after 5 seconds
|
|
||||||
@inputTimeout = setTimeout @onTypingEnd, 5000
|
|
||||||
|
|
||||||
@onTypingStart() if @isTyping
|
|
||||||
|
|
||||||
onTypingStart: ->
|
onTypingStart: ->
|
||||||
|
|
||||||
|
clearTimeout(@isTypingTimeout) if @isTypingTimeout
|
||||||
|
|
||||||
|
# fire typingEnd after 5 seconds
|
||||||
|
@isTypingTimeout = setTimeout @onTypingEnd, 1500
|
||||||
|
|
||||||
# send typing start event
|
# send typing start event
|
||||||
@isTyping = true
|
if !@isTyping
|
||||||
@send 'typing_start'
|
@isTyping = true
|
||||||
|
@send 'chat_session_typing', {session_id: @session_id}
|
||||||
|
|
||||||
onTypingEnd: =>
|
onTypingEnd: =>
|
||||||
# send typing end event
|
|
||||||
@isTyping = false
|
@isTyping = false
|
||||||
@send 'typing_end'
|
|
||||||
|
|
||||||
onSubmit: (event) =>
|
onSubmit: (event) =>
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -167,9 +181,10 @@ do($ = window.jQuery, window) ->
|
||||||
@isTyping = false
|
@isTyping = false
|
||||||
|
|
||||||
# send message event
|
# send message event
|
||||||
@send 'message',
|
@send 'chat_session_message',
|
||||||
body: message
|
content: message
|
||||||
id: @_messageCount
|
id: @_messageCount
|
||||||
|
session_id: @session_id
|
||||||
|
|
||||||
receiveMessage: (data) =>
|
receiveMessage: (data) =>
|
||||||
# hide writing indicator
|
# hide writing indicator
|
||||||
|
@ -180,7 +195,7 @@ do($ = window.jQuery, window) ->
|
||||||
@lastAddedType = 'message--agent'
|
@lastAddedType = 'message--agent'
|
||||||
unread = document.hidden ? " zammad-chat-message--unread" : ""
|
unread = document.hidden ? " zammad-chat-message--unread" : ""
|
||||||
@el.find('.zammad-chat-body').append @view('message')
|
@el.find('.zammad-chat-body').append @view('message')
|
||||||
message: data.body
|
message: data.message.content
|
||||||
id: data.id
|
id: data.id
|
||||||
from: 'agent'
|
from: 'agent'
|
||||||
@scrollToBottom()
|
@scrollToBottom()
|
||||||
|
@ -275,7 +290,7 @@ 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'))
|
||||||
|
|
||||||
connect: ->
|
connect: ->
|
||||||
@send('chat_init')
|
@send('chat_session_init')
|
||||||
|
|
||||||
reconnect: =>
|
reconnect: =>
|
||||||
# set status to connecting
|
# set status to connecting
|
||||||
|
|
|
@ -1,66 +1,3 @@
|
||||||
if (!window.zammadChatTemplates) {
|
|
||||||
window.zammadChatTemplates = {};
|
|
||||||
}
|
|
||||||
window.zammadChatTemplates["agent"] = function (__obj) {
|
|
||||||
if (!__obj) __obj = {};
|
|
||||||
var __out = [], __capture = function(callback) {
|
|
||||||
var out = __out, result;
|
|
||||||
__out = [];
|
|
||||||
callback.call(this);
|
|
||||||
result = __out.join('');
|
|
||||||
__out = out;
|
|
||||||
return __safe(result);
|
|
||||||
}, __sanitize = function(value) {
|
|
||||||
if (value && value.ecoSafe) {
|
|
||||||
return value;
|
|
||||||
} else if (typeof value !== 'undefined' && value != null) {
|
|
||||||
return __escape(value);
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
|
|
||||||
__safe = __obj.safe = function(value) {
|
|
||||||
if (value && value.ecoSafe) {
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
if (!(typeof value !== 'undefined' && value != null)) value = '';
|
|
||||||
var result = new String(value);
|
|
||||||
result.ecoSafe = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (!__escape) {
|
|
||||||
__escape = __obj.escape = function(value) {
|
|
||||||
return ('' + value)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(function() {
|
|
||||||
(function() {
|
|
||||||
__out.push('<img class="zammad-chat-agent-avatar" src="');
|
|
||||||
|
|
||||||
__out.push(__sanitize(this.agent.avatar));
|
|
||||||
|
|
||||||
__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> ');
|
|
||||||
|
|
||||||
__out.push(this.agentPhrase);
|
|
||||||
|
|
||||||
__out.push('\n</span>');
|
|
||||||
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
}).call(__obj);
|
|
||||||
__obj.safe = __objSafe, __obj.escape = __escape;
|
|
||||||
return __out.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||||
|
|
||||||
(function($, window) {
|
(function($, window) {
|
||||||
|
@ -151,6 +88,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
}).autoGrow({
|
}).autoGrow({
|
||||||
extraLine: false
|
extraLine: false
|
||||||
});
|
});
|
||||||
|
this.session_id = void 0;
|
||||||
if (!window.WebSocket) {
|
if (!window.WebSocket) {
|
||||||
console.log('Zammad Chat: Browser not supported');
|
console.log('Zammad Chat: Browser not supported');
|
||||||
return;
|
return;
|
||||||
|
@ -161,7 +99,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
this.ws.onopen = (function(_this) {
|
this.ws.onopen = (function(_this) {
|
||||||
return function() {
|
return function() {
|
||||||
console.log('ws connected');
|
console.log('ws connected');
|
||||||
return _this.send("chat_status");
|
return _this.send('chat_status');
|
||||||
};
|
};
|
||||||
})(this);
|
})(this);
|
||||||
this.ws.onmessage = this.onWebSocketMessage;
|
this.ws.onmessage = this.onWebSocketMessage;
|
||||||
|
@ -184,45 +122,76 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.send = function(action, data) {
|
ZammadChat.prototype.send = function(event, data) {
|
||||||
var pipe;
|
var pipe;
|
||||||
|
console.log('debug', 'ws:send', event, data);
|
||||||
pipe = JSON.stringify({
|
pipe = JSON.stringify({
|
||||||
action: action,
|
event: event,
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
return this.ws.send(pipe);
|
return this.ws.send(pipe);
|
||||||
};
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.onWebSocketMessage = function(e) {
|
ZammadChat.prototype.onWebSocketMessage = function(e) {
|
||||||
var pipe;
|
var delay, i, len, pipe, pipes;
|
||||||
pipe = JSON.parse(e.data);
|
pipes = JSON.parse(e.data);
|
||||||
console.log('debug', 'ws:onmessage', pipe);
|
console.log('debug', 'ws:onmessage', pipes);
|
||||||
switch (pipe.action) {
|
for (i = 0, len = pipes.length; i < len; i++) {
|
||||||
case 'chat_message':
|
pipe = pipes[i];
|
||||||
return this.receiveMessage(pipe.data);
|
switch (pipe.event) {
|
||||||
case 'chat_typing_start':
|
case 'chat_session_message':
|
||||||
return this.onAgentTypingStart();
|
if (pipe.data.self_written) {
|
||||||
case 'chat_typing_end':
|
return;
|
||||||
return this.onAgentTypingEnd();
|
}
|
||||||
case 'chat_init':
|
this.receiveMessage(pipe.data);
|
||||||
switch (pipe.data.state) {
|
break;
|
||||||
case 'ok':
|
case 'chat_session_typing':
|
||||||
return this.onConnectionEstablished(pipe.data.agent);
|
if (pipe.data.self_written) {
|
||||||
case 'queue':
|
return;
|
||||||
return this.onQueue(pipe.data.position);
|
}
|
||||||
}
|
this.onAgentTypingStart();
|
||||||
break;
|
if (this.stopTypingId) {
|
||||||
case 'chat_status':
|
clearTimeout(this.stopTypingId);
|
||||||
switch (pipe.data.state) {
|
}
|
||||||
case 'ok':
|
delay = (function(_this) {
|
||||||
return this.onReady();
|
return function() {
|
||||||
case 'offline':
|
return _this.onAgentTypingEnd();
|
||||||
return console.log('Zammad Chat: No agent online');
|
};
|
||||||
case 'chat_disabled':
|
})(this);
|
||||||
return console.log('Zammad Chat: Chat is disabled');
|
this.stopTypingId = setTimeout(delay, 3000);
|
||||||
case 'no_seats_available':
|
break;
|
||||||
return console.log('Zammad Chat: Too many clients in queue. Clients in queue: ', pipe.data.queue);
|
case 'chat_session_start':
|
||||||
}
|
switch (pipe.data.state) {
|
||||||
|
case 'ok':
|
||||||
|
this.onConnectionEstablished(pipe.data.agent);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'chat_session_init':
|
||||||
|
switch (pipe.data.state) {
|
||||||
|
case 'ok':
|
||||||
|
this.onConnectionEstablished(pipe.data.agent);
|
||||||
|
break;
|
||||||
|
case 'queue':
|
||||||
|
this.onQueue(pipe.data.position);
|
||||||
|
this.session_id = pipe.data.session_id;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'chat_status':
|
||||||
|
switch (pipe.data.state) {
|
||||||
|
case 'online':
|
||||||
|
this.onReady();
|
||||||
|
console.log('Zammad Chat: ready');
|
||||||
|
break;
|
||||||
|
case 'offline':
|
||||||
|
console.log('Zammad Chat: No agent online');
|
||||||
|
break;
|
||||||
|
case 'chat_disabled':
|
||||||
|
console.log('Zammad Chat: Chat is disabled');
|
||||||
|
break;
|
||||||
|
case 'no_seats_available':
|
||||||
|
console.log('Zammad Chat: Too many clients in queue. Clients in queue: ', pipe.data.queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -234,23 +203,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
|
|
||||||
ZammadChat.prototype.onInput = function() {
|
ZammadChat.prototype.onInput = function() {
|
||||||
this.el.find('.zammad-chat-message--unread').removeClass('zammad-chat-message--unread');
|
this.el.find('.zammad-chat-message--unread').removeClass('zammad-chat-message--unread');
|
||||||
if (this.inputTimeout) {
|
return this.onTypingStart();
|
||||||
clearTimeout(this.inputTimeout);
|
|
||||||
}
|
|
||||||
this.inputTimeout = setTimeout(this.onTypingEnd, 5000);
|
|
||||||
if (this.isTyping) {
|
|
||||||
return this.onTypingStart();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.onTypingStart = function() {
|
ZammadChat.prototype.onTypingStart = function() {
|
||||||
this.isTyping = true;
|
if (this.isTypingTimeout) {
|
||||||
return this.send('typing_start');
|
clearTimeout(this.isTypingTimeout);
|
||||||
|
}
|
||||||
|
this.isTypingTimeout = setTimeout(this.onTypingEnd, 1500);
|
||||||
|
if (!this.isTyping) {
|
||||||
|
this.isTyping = true;
|
||||||
|
return this.send('chat_session_typing', {
|
||||||
|
session_id: this.session_id
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.onTypingEnd = function() {
|
ZammadChat.prototype.onTypingEnd = function() {
|
||||||
this.isTyping = false;
|
return this.isTyping = false;
|
||||||
return this.send('typing_end');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.onSubmit = function(event) {
|
ZammadChat.prototype.onSubmit = function(event) {
|
||||||
|
@ -280,9 +250,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
this.el.find('.zammad-chat-input').val('');
|
this.el.find('.zammad-chat-input').val('');
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
this.isTyping = false;
|
this.isTyping = false;
|
||||||
return this.send('message', {
|
return this.send('chat_session_message', {
|
||||||
body: message,
|
content: message,
|
||||||
id: this._messageCount
|
id: this._messageCount,
|
||||||
|
session_id: this.session_id
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -295,7 +266,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
" zammad-chat-message--unread": ""
|
" zammad-chat-message--unread": ""
|
||||||
};
|
};
|
||||||
this.el.find('.zammad-chat-body').append(this.view('message')({
|
this.el.find('.zammad-chat-body').append(this.view('message')({
|
||||||
message: data.body,
|
message: data.message.content,
|
||||||
id: data.id,
|
id: data.id,
|
||||||
from: 'agent'
|
from: 'agent'
|
||||||
}));
|
}));
|
||||||
|
@ -404,7 +375,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.connect = function() {
|
ZammadChat.prototype.connect = function() {
|
||||||
return this.send('chat_init');
|
return this.send('chat_session_init');
|
||||||
};
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.reconnect = function() {
|
ZammadChat.prototype.reconnect = function() {
|
||||||
|
@ -456,6 +427,69 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
});
|
});
|
||||||
})(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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
(function() {
|
||||||
|
__out.push('<img class="zammad-chat-agent-avatar" src="');
|
||||||
|
|
||||||
|
__out.push(__sanitize(this.agent.avatar));
|
||||||
|
|
||||||
|
__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> ');
|
||||||
|
|
||||||
|
__out.push(this.agentPhrase);
|
||||||
|
|
||||||
|
__out.push('\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):
|
||||||
|
|
2
public/assets/chat/chat.min.js
vendored
2
public/assets/chat/chat.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -94,8 +94,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="jquery-2.1.4.min.js"></script>
|
<script src="jquery-2.1.4.min.js"></script>
|
||||||
|
<!--
|
||||||
<script src="node_modules/sass.js/dist/sass.js"></script>
|
<script src="node_modules/sass.js/dist/sass.js"></script>
|
||||||
|
-->
|
||||||
<script src="chat.js"></script>
|
<script src="chat.js"></script>
|
||||||
|
<!--
|
||||||
<script>
|
<script>
|
||||||
var sass = new Sass();
|
var sass = new Sass();
|
||||||
var scss = '';
|
var scss = '';
|
||||||
|
@ -125,7 +128,8 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
-->
|
||||||
|
<script>
|
||||||
|
|
||||||
$('.settings :input').on({
|
$('.settings :input').on({
|
||||||
change: function(){
|
change: function(){
|
||||||
|
|
|
@ -136,6 +136,9 @@ EventMachine.run {
|
||||||
# check if connection not already exists
|
# check if connection not already exists
|
||||||
next if !@clients[client_id]
|
next if !@clients[client_id]
|
||||||
|
|
||||||
|
Sessions.touch(client_id)
|
||||||
|
@clients[client_id][:last_ping] = Time.now.utc.to_i
|
||||||
|
|
||||||
# spool messages for new connects
|
# spool messages for new connects
|
||||||
if data['spool']
|
if data['spool']
|
||||||
Sessions.spool_create(msg)
|
Sessions.spool_create(msg)
|
||||||
|
@ -201,8 +204,6 @@ EventMachine.run {
|
||||||
|
|
||||||
# remember ping, send pong back
|
# remember ping, send pong back
|
||||||
elsif data['action'] == 'ping'
|
elsif data['action'] == 'ping'
|
||||||
Sessions.touch(client_id)
|
|
||||||
@clients[client_id][:last_ping] = Time.now.utc.to_i
|
|
||||||
message = {
|
message = {
|
||||||
action: 'pong',
|
action: 'pong',
|
||||||
}
|
}
|
||||||
|
@ -255,6 +256,12 @@ EventMachine.run {
|
||||||
log 'notice', 'do not send broadcast to it self', client_id
|
log 'notice', 'do not send broadcast to it self', client_id
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elsif data['event']
|
||||||
|
message = Sessions::Event.run(data['event'], data, @clients[client_id][:session], client_id)
|
||||||
|
if message
|
||||||
|
websocket_send(client_id, message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue