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">
|
||||
<%- @T(item.name) %>
|
||||
</span>
|
||||
<% if item.counter isnt undefined: %>
|
||||
<% if item.counter isnt undefined && item.counter isnt 0: %>
|
||||
<span class="counter badge badge--big"><%= item.counter %></span>
|
||||
<% end %>
|
||||
<% 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
|
||||
).autoGrow { extraLine: false }
|
||||
|
||||
@session_id = undefined
|
||||
|
||||
if !window.WebSocket
|
||||
console.log('Zammad Chat: Browser not supported')
|
||||
return
|
||||
|
@ -61,7 +63,7 @@ do($ = window.jQuery, window) ->
|
|||
|
||||
@ws.onopen = =>
|
||||
console.log('ws connected')
|
||||
@send "chat_status"
|
||||
@send 'chat_status'
|
||||
|
||||
@ws.onmessage = @onWebSocketMessage
|
||||
|
||||
|
@ -76,40 +78,52 @@ do($ = window.jQuery, window) ->
|
|||
event.preventDefault()
|
||||
@sendMessage()
|
||||
|
||||
send: (action, data) =>
|
||||
send: (event, data) =>
|
||||
console.log 'debug', 'ws:send', event, data
|
||||
pipe = JSON.stringify
|
||||
action: action
|
||||
event: event
|
||||
data: data
|
||||
|
||||
@ws.send pipe
|
||||
|
||||
onWebSocketMessage: (e) =>
|
||||
pipe = JSON.parse( e.data )
|
||||
console.log 'debug', 'ws:onmessage', pipe
|
||||
pipes = JSON.parse( e.data )
|
||||
console.log 'debug', 'ws:onmessage', pipes
|
||||
|
||||
switch pipe.action
|
||||
when 'chat_message'
|
||||
@receiveMessage pipe.data
|
||||
when 'chat_typing_start'
|
||||
@onAgentTypingStart()
|
||||
when 'chat_typing_end'
|
||||
@onAgentTypingEnd()
|
||||
when 'chat_init'
|
||||
switch pipe.data.state
|
||||
when 'ok'
|
||||
@onConnectionEstablished pipe.data.agent
|
||||
when 'queue'
|
||||
@onQueue pipe.data.position
|
||||
when 'chat_status'
|
||||
switch pipe.data.state
|
||||
when 'ok'
|
||||
@onReady()
|
||||
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
|
||||
for pipe in pipes
|
||||
switch pipe.event
|
||||
when 'chat_session_message'
|
||||
return if pipe.data.self_written
|
||||
@receiveMessage pipe.data
|
||||
when 'chat_session_typing'
|
||||
return if pipe.data.self_written
|
||||
@onAgentTypingStart()
|
||||
if @stopTypingId
|
||||
clearTimeout(@stopTypingId)
|
||||
delay = =>
|
||||
@onAgentTypingEnd()
|
||||
@stopTypingId = setTimeout(delay, 3000)
|
||||
when 'chat_session_start'
|
||||
switch pipe.data.state
|
||||
when 'ok'
|
||||
@onConnectionEstablished pipe.data.agent
|
||||
when 'chat_session_init'
|
||||
switch pipe.data.state
|
||||
when 'ok'
|
||||
@onConnectionEstablished pipe.data.agent
|
||||
when '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: =>
|
||||
@show() if @options.show
|
||||
|
@ -119,22 +133,22 @@ do($ = window.jQuery, window) ->
|
|||
@el.find('.zammad-chat-message--unread')
|
||||
.removeClass 'zammad-chat-message--unread'
|
||||
|
||||
clearTimeout(@inputTimeout) if @inputTimeout
|
||||
|
||||
# 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
|
||||
@isTyping = true
|
||||
@send 'typing_start'
|
||||
if !@isTyping
|
||||
@isTyping = true
|
||||
@send 'chat_session_typing', {session_id: @session_id}
|
||||
|
||||
onTypingEnd: =>
|
||||
# send typing end event
|
||||
@isTyping = false
|
||||
@send 'typing_end'
|
||||
|
||||
onSubmit: (event) =>
|
||||
event.preventDefault()
|
||||
|
@ -167,9 +181,10 @@ do($ = window.jQuery, window) ->
|
|||
@isTyping = false
|
||||
|
||||
# send message event
|
||||
@send 'message',
|
||||
body: message
|
||||
@send 'chat_session_message',
|
||||
content: message
|
||||
id: @_messageCount
|
||||
session_id: @session_id
|
||||
|
||||
receiveMessage: (data) =>
|
||||
# hide writing indicator
|
||||
|
@ -180,7 +195,7 @@ do($ = window.jQuery, window) ->
|
|||
@lastAddedType = 'message--agent'
|
||||
unread = document.hidden ? " zammad-chat-message--unread" : ""
|
||||
@el.find('.zammad-chat-body').append @view('message')
|
||||
message: data.body
|
||||
message: data.message.content
|
||||
id: data.id
|
||||
from: 'agent'
|
||||
@scrollToBottom()
|
||||
|
@ -275,7 +290,7 @@ do($ = window.jQuery, window) ->
|
|||
@el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'))
|
||||
|
||||
connect: ->
|
||||
@send('chat_init')
|
||||
@send('chat_session_init')
|
||||
|
||||
reconnect: =>
|
||||
# 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); }; };
|
||||
|
||||
(function($, window) {
|
||||
|
@ -151,6 +88,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
}).autoGrow({
|
||||
extraLine: false
|
||||
});
|
||||
this.session_id = void 0;
|
||||
if (!window.WebSocket) {
|
||||
console.log('Zammad Chat: Browser not supported');
|
||||
return;
|
||||
|
@ -161,7 +99,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
this.ws.onopen = (function(_this) {
|
||||
return function() {
|
||||
console.log('ws connected');
|
||||
return _this.send("chat_status");
|
||||
return _this.send('chat_status');
|
||||
};
|
||||
})(this);
|
||||
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;
|
||||
console.log('debug', 'ws:send', event, data);
|
||||
pipe = JSON.stringify({
|
||||
action: action,
|
||||
event: event,
|
||||
data: data
|
||||
});
|
||||
return this.ws.send(pipe);
|
||||
};
|
||||
|
||||
ZammadChat.prototype.onWebSocketMessage = function(e) {
|
||||
var pipe;
|
||||
pipe = JSON.parse(e.data);
|
||||
console.log('debug', 'ws:onmessage', pipe);
|
||||
switch (pipe.action) {
|
||||
case 'chat_message':
|
||||
return this.receiveMessage(pipe.data);
|
||||
case 'chat_typing_start':
|
||||
return this.onAgentTypingStart();
|
||||
case 'chat_typing_end':
|
||||
return this.onAgentTypingEnd();
|
||||
case 'chat_init':
|
||||
switch (pipe.data.state) {
|
||||
case 'ok':
|
||||
return this.onConnectionEstablished(pipe.data.agent);
|
||||
case 'queue':
|
||||
return this.onQueue(pipe.data.position);
|
||||
}
|
||||
break;
|
||||
case 'chat_status':
|
||||
switch (pipe.data.state) {
|
||||
case 'ok':
|
||||
return this.onReady();
|
||||
case 'offline':
|
||||
return console.log('Zammad Chat: No agent online');
|
||||
case 'chat_disabled':
|
||||
return console.log('Zammad Chat: Chat is disabled');
|
||||
case 'no_seats_available':
|
||||
return console.log('Zammad Chat: Too many clients in queue. Clients in queue: ', pipe.data.queue);
|
||||
}
|
||||
var delay, i, len, pipe, pipes;
|
||||
pipes = JSON.parse(e.data);
|
||||
console.log('debug', 'ws:onmessage', pipes);
|
||||
for (i = 0, len = pipes.length; i < len; i++) {
|
||||
pipe = pipes[i];
|
||||
switch (pipe.event) {
|
||||
case 'chat_session_message':
|
||||
if (pipe.data.self_written) {
|
||||
return;
|
||||
}
|
||||
this.receiveMessage(pipe.data);
|
||||
break;
|
||||
case 'chat_session_typing':
|
||||
if (pipe.data.self_written) {
|
||||
return;
|
||||
}
|
||||
this.onAgentTypingStart();
|
||||
if (this.stopTypingId) {
|
||||
clearTimeout(this.stopTypingId);
|
||||
}
|
||||
delay = (function(_this) {
|
||||
return function() {
|
||||
return _this.onAgentTypingEnd();
|
||||
};
|
||||
})(this);
|
||||
this.stopTypingId = setTimeout(delay, 3000);
|
||||
break;
|
||||
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() {
|
||||
this.el.find('.zammad-chat-message--unread').removeClass('zammad-chat-message--unread');
|
||||
if (this.inputTimeout) {
|
||||
clearTimeout(this.inputTimeout);
|
||||
}
|
||||
this.inputTimeout = setTimeout(this.onTypingEnd, 5000);
|
||||
if (this.isTyping) {
|
||||
return this.onTypingStart();
|
||||
}
|
||||
return this.onTypingStart();
|
||||
};
|
||||
|
||||
ZammadChat.prototype.onTypingStart = function() {
|
||||
this.isTyping = true;
|
||||
return this.send('typing_start');
|
||||
if (this.isTypingTimeout) {
|
||||
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() {
|
||||
this.isTyping = false;
|
||||
return this.send('typing_end');
|
||||
return this.isTyping = false;
|
||||
};
|
||||
|
||||
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.scrollToBottom();
|
||||
this.isTyping = false;
|
||||
return this.send('message', {
|
||||
body: message,
|
||||
id: this._messageCount
|
||||
return this.send('chat_session_message', {
|
||||
content: message,
|
||||
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": ""
|
||||
};
|
||||
this.el.find('.zammad-chat-body').append(this.view('message')({
|
||||
message: data.body,
|
||||
message: data.message.content,
|
||||
id: data.id,
|
||||
from: 'agent'
|
||||
}));
|
||||
|
@ -404,7 +375,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
};
|
||||
|
||||
ZammadChat.prototype.connect = function() {
|
||||
return this.send('chat_init');
|
||||
return this.send('chat_session_init');
|
||||
};
|
||||
|
||||
ZammadChat.prototype.reconnect = function() {
|
||||
|
@ -456,6 +427,69 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
});
|
||||
})(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):
|
||||
|
|
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>
|
||||
|
||||
<script src="jquery-2.1.4.min.js"></script>
|
||||
<!--
|
||||
<script src="node_modules/sass.js/dist/sass.js"></script>
|
||||
-->
|
||||
<script src="chat.js"></script>
|
||||
<!--
|
||||
<script>
|
||||
var sass = new Sass();
|
||||
var scss = '';
|
||||
|
@ -125,7 +128,8 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
-->
|
||||
<script>
|
||||
|
||||
$('.settings :input').on({
|
||||
change: function(){
|
||||
|
|
|
@ -136,6 +136,9 @@ EventMachine.run {
|
|||
# check if connection not already exists
|
||||
next if !@clients[client_id]
|
||||
|
||||
Sessions.touch(client_id)
|
||||
@clients[client_id][:last_ping] = Time.now.utc.to_i
|
||||
|
||||
# spool messages for new connects
|
||||
if data['spool']
|
||||
Sessions.spool_create(msg)
|
||||
|
@ -201,8 +204,6 @@ EventMachine.run {
|
|||
|
||||
# remember ping, send pong back
|
||||
elsif data['action'] == 'ping'
|
||||
Sessions.touch(client_id)
|
||||
@clients[client_id][:last_ping] = Time.now.utc.to_i
|
||||
message = {
|
||||
action: 'pong',
|
||||
}
|
||||
|
@ -255,6 +256,12 @@ EventMachine.run {
|
|||
log 'notice', 'do not send broadcast to it self', client_id
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue