Added support to search for chat sessions. Added set name and tags for chats.

This commit is contained in:
Martin Edenhofer 2017-12-18 04:36:56 +01:00
parent 69ccf09906
commit 9c54b3382d
13 changed files with 417 additions and 87 deletions

View file

@ -35,7 +35,7 @@ class App.CustomerChat extends App.Controller
active_agent_ids: []
@render()
@on 'layout-has-changed', @propagateLayoutChange
@on('layout-has-changed', @propagateLayoutChange)
# update navbar on new status
@bind('chat_status_agent', (data) =>
@ -163,6 +163,11 @@ class App.CustomerChat extends App.Controller
@title 'Customer Chat', true
@navupdate '#customer_chat'
if params.session_id && App.ChatSession.exists(params.session_id)
session = App.ChatSession.find(params.session_id)
@addChat(session)
@navigate '#customer_chat'
active: (state) =>
return @shown if state is undefined
@shown = state
@ -264,10 +269,11 @@ class App.CustomerChat extends App.Controller
addChat: (session) ->
return if @chatWindows[session.session_id]
chat = new ChatWindow
chat = new ChatWindow(
session: session
removeCallback: @removeChat
messageCallback: @updateNavMenu
)
@workspace.append chat.el
chat.render()
@ -289,7 +295,7 @@ class App.CustomerChat extends App.Controller
propagateLayoutChange: (event) =>
# adjust scroll position on layoutChange
for session_id, chat of @chatWindows
chat.trigger 'layout-changed'
chat.trigger('layout-changed')
acceptChat: =>
return if @windowCount() >= @maxChatWindows
@ -324,19 +330,6 @@ class App.CustomerChat extends App.Controller
currentPosition: =>
@$('.main').scrollTop()
class CustomerChatRouter extends App.ControllerPermanent
requiredPermission: 'chat.agent'
constructor: (params) ->
super
App.TaskManager.execute(
key: 'CustomerChat'
controller: 'CustomerChat'
params: {}
show: true
persistent: true
)
class ChatWindow extends App.Controller
className: 'chat-window'
@ -348,6 +341,8 @@ class ChatWindow extends App.Controller
'click .js-close': 'close'
'click .js-disconnect': 'disconnect'
'click .js-scrollHint': 'onScrollHintClick'
'click .js-info': 'toggleMeta'
'submit .js-metaForm': 'sendMetaForm'
elements:
'.js-customerChatInput': 'input'
@ -355,8 +350,11 @@ class ChatWindow extends App.Controller
'.js-close': 'closeButton'
'.js-disconnect': 'disconnectButton'
'.js-body': 'body'
'.js-meta': 'meta'
'.js-name': 'metaName'
'.js-scrollHolder': 'scrollHolder'
'.js-scrollHint': 'scrollHint'
'.js-metaForm': 'metaForm'
sounds:
message: new Audio('assets/sounds/chat_message.mp3')
@ -374,9 +372,11 @@ class ChatWindow extends App.Controller
@scrollSnapTolerance = 10 # pixels
@chat = App.Chat.find(@session.chat_id)
@name = "#{@chat.displayName()} ##{@session.id}"
@name = @chat.displayName()
if @session && !_.isEmpty(@session.name)
@name = @session.name
@on 'layout-change', @onLayoutChange
@on('layout-change', @onLayoutChange)
@bind('chat_session_typing', (data) =>
return if data.session_id isnt @session.session_id
@ -413,12 +413,44 @@ class ChatWindow extends App.Controller
onLayoutChange: =>
@scrollToBottom()
render: ->
@html App.view('customer_chat/chat_window')
name: @name
toggleMeta: =>
if @meta.hasClass('hidden')
@showMeta()
else
@hideMeta()
@el.one 'transitionend', @onTransitionend
@scrollHolder.scroll @detectScrolledtoBottom
hideMeta: =>
@body.removeClass('hidden')
@meta.addClass('hidden')
@sendMetaForm()
showMeta: =>
@body.addClass('hidden')
@meta.removeClass('hidden')
sendMetaForm: (e) =>
if e
e.preventDefault()
params = @formParam(@metaForm)
App.WebSocket.send(
event:'chat_session_update'
data:
session_id: @session.session_id
name: params.name
tags: params.tags
)
@metaName.text(params.name)
render: ->
@html App.view('customer_chat/chat_window')(
name: @name
session: @session
)
@el.one('transitionend', @onTransitionend)
@scrollHolder.scroll(@detectScrolledtoBottom)
# force repaint
@el.prop('offsetHeight')
@ -426,18 +458,24 @@ class ChatWindow extends App.Controller
# @addMessage 'Hello. My name is Roger, how can I help you?', 'agent'
if @session
# set chat to offline if state is already closed
activeChat = true
if @session.state is 'closed'
activeChat = false
if @session && @session.preferences && @session.preferences.url
@addNoticeMessage(@session.preferences.url)
@addNoticeMessage(@session.preferences.url, undefined, activeChat)
if @session.messages
for message in @session.messages
if message.created_by_id
@addMessage message.content, 'agent'
@addMessage(message.content, 'agent', false, activeChat)
else
@addMessage message.content, 'customer'
@addMessage(message.content, 'customer', false, activeChat)
# send init reply
if !@session.messages || _.isEmpty(@session.messages)
if activeChat && _.isEmpty(@session.messages)
preferences = @Session.get('preferences')
if preferences.chat && preferences.chat.phrase
phrases = preferences.chat.phrase[@session.chat_id]
@ -447,20 +485,9 @@ class ChatWindow extends App.Controller
@input.html(phrase)
@sendMessage(1600)
@$('.js-info').popover(
trigger: 'hover'
html: true
animation: false
delay: 0
placement: 'bottom'
container: 'body' # place in body do prevent it from animating
title: ->
App.i18n.translateContent('Details')
content: =>
App.view('customer_chat/chat_window_info')(
session: @session
)
)
# set chat to offline if state is already closed
if !activeChat
@goOffline()
# show text module UI
new App.WidgetTextModule(
@ -470,6 +497,18 @@ class ChatWindow extends App.Controller
config: App.Config.all()
)
configureAttributesOutbound = [
{ name: 'name', display: 'Name', tag: 'input', null: true, },
{ name: 'tags', display: 'Tags', tag: 'tag', null: true, },
]
new App.ControllerForm(
el: @$('.js-metaForm')
model:
configure_attributes: configureAttributesOutbound
className: ''
params: @session
)
focus: =>
@input.focus()
@ -498,7 +537,8 @@ class ChatWindow extends App.Controller
@goOffline()
close: =>
@el.one 'transitionend', { callback: @release }, @onTransitionend
@sendMetaForm()
@el.one('transitionend', { callback: @release }, @onTransitionend)
@el.removeClass('is-open')
if @removeCallback
@removeCallback(@session.session_id)
@ -577,7 +617,8 @@ class ChatWindow extends App.Controller
)
@delay(send, delay)
@addMessage content, 'agent'
@hideMeta()
@addMessage(content, 'agent')
@input.html('')
updateModified: (state) =>
@ -614,18 +655,19 @@ class ChatWindow extends App.Controller
@messageCallback(@session.session_id)
@unreadMessagesCounter = 0
addMessage: (message, sender, isNew) =>
@maybeAddTimestamp()
addMessage: (message, sender, isNew, useMaybeAddTimestamp = true) =>
@maybeAddTimestamp() if useMaybeAddTimestamp
@lastAddedType = sender
@body.append App.view('customer_chat/chat_message')
@body.append App.view('customer_chat/chat_message')(
message: message
sender: sender
isNew: isNew
timestamp: Date.now()
)
@scrollToBottom showHint: true
@scrollToBottom(showHint: true)
showWritingLoader: =>
if !@isTyping
@ -667,33 +709,37 @@ class ChatWindow extends App.Controller
@lastAddedType = 'timestamp'
addTimestamp: (label, time) =>
@body.append App.view('customer_chat/chat_timestamp')
@body.append App.view('customer_chat/chat_timestamp')(
label: label
time: time
)
updateLastTimestamp: (label, time) ->
@body
.find('.js-timestamp')
.last()
.replaceWith App.view('customer_chat/chat_timestamp')
.replaceWith App.view('customer_chat/chat_timestamp')(
label: label
time: time
)
addStatusMessage: (message, args) ->
@maybeAddTimestamp()
addStatusMessage: (message, args, useMaybeAddTimestamp = true) ->
@maybeAddTimestamp() if useMaybeAddTimestamp
@body.append App.view('customer_chat/chat_status_message')
@body.append App.view('customer_chat/chat_status_message')(
message: message
args: args
)
@scrollToBottom()
addNoticeMessage: (message, args) ->
@maybeAddTimestamp()
addNoticeMessage: (message, args, useMaybeAddTimestamp = true) ->
@maybeAddTimestamp() if useMaybeAddTimestamp
@body.append App.view('customer_chat/chat_notice_message')
@body.append App.view('customer_chat/chat_notice_message')(
message: message
args: args
)
@scrollToBottom()
@ -784,6 +830,24 @@ class Setting extends App.ControllerModal
msg: App.i18n.translateContent(data.message)
)
class CustomerChatRouter extends App.ControllerPermanent
requiredPermission: 'chat.agent'
constructor: (params) ->
super
# cleanup params
clean_params =
session_id: params.session_id
App.TaskManager.execute(
key: 'CustomerChat'
controller: 'CustomerChat'
params: clean_params
show: true
persistent: true
)
App.Config.set('customer_chat', CustomerChatRouter, 'Routes')
App.Config.set('customer_chat/session/:session_id', CustomerChatRouter, 'Routes')
App.Config.set('CustomerChat', { controller: 'CustomerChat', permission: ['chat.agent'] }, 'permanentTask')
App.Config.set('CustomerChat', { prio: 1200, parent: '', name: 'Customer Chat', target: '#customer_chat', key: 'CustomerChat', shown: false, permission: ['chat.agent'], class: 'chat' }, 'NavBar')

View file

@ -79,6 +79,7 @@ class App.Search extends App.Controller
@tabs = []
for model in App.Config.get('models_searchable')
model = model.replace(/::/, '')
tab =
name: model
model: model

View file

@ -0,0 +1,32 @@
class App.ChatSession extends App.Model
@configure 'ChatSession', 'name', 'note'
@extend Spine.Model.Ajax
@url: @apiPath + '/chat_sessions'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false }
{ name: 'state', display: 'State', readonly: 1 }
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 }
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 }
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 }
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }
]
@configure_overview = [
'name',
'state',
'created_at',
]
uiUrl: ->
"#customer_chat/session/#{@id}"
searchResultAttributes: ->
displayName = ''
if !_.isEmpty(@name)
displayName = @displayName()
display: "##{@id} #{displayName}"
id: @id
class: 'chat_session chat_session-popover'
url: @uiUrl()
icon: 'chat'

View file

@ -7,9 +7,7 @@
</div>
</div>
<div class="chat-name">
<%= @name %> <div class="status-badge js-info">
<div class="info-badge"><%- @Icon('info') %></div>
</div>
<span class="js-name js-info u-clickable"><%= @name %><span> #<%= @session.id %>
</div>
<div class="chat-disconnect js-disconnect">
<div class="btn btn--action btn--small"><%- @T('disconnect') %></div>
@ -24,6 +22,25 @@
</div>
<div class="chat-body-holder js-scrollHolder">
<div class="chat-body js-body"></div>
<div class="chat-body js-meta hidden">
<% if @session: %>
<ul>
<li><%- @T('Created at') %>: <%- @Ttimestamp(@session.created_at) %></li>
<% if @session && @session.preferences: %>
<% if @session.preferences.geo_ip: %>
<li>GeoIP: <%= @session.preferences.geo_ip.country_name %> <%= @session.preferences.geo_ip.city_name %></li>
<% end %>
<% if @session.preferences.remote_ip: %>
<li>IP: <%= @session.preferences.remote_ip %></li>
<% end %>
<% if @session.preferences.dns_name: %>
<li>DNS: <%= @session.preferences.dns_name %></li>
<% end %>
<% end %>
</ul>
<% end %>
<form class="js-metaForm" style="max-width: 200px; width: 100%;"></form>
</div>
</div>
<div class="chat-controls">
<div class="chat-input">

View file

@ -1,17 +0,0 @@
<hr>
<ul>
<% if @session: %>
<li><%- @T('Created at') %>: <%- @Ttimestamp(@session.created_at) %>
<% end %>
<% if @session && @session.preferences: %>
<% if @session.preferences.geo_ip: %>
<li>GeoIP: <%= @session.preferences.geo_ip.country_name %> <%= @session.preferences.geo_ip.city_name %>
<% end %>
<% if @session.preferences.remote_ip: %>
<li>IP: <%= @session.preferences.remote_ip %>
<% end %>
<% if @session.preferences.dns_name: %>
<li>DNS: <%= @session.preferences.dns_name %>
<% end %>
<% end %>
</ul>

View file

@ -33,9 +33,10 @@ class SearchController < ApplicationController
objects_in_order = []
objects_in_order_hash = {}
objects.each do |object|
preferences = object.constantize.search_preferences(current_user)
local_class = object.constantize
preferences = local_class.search_preferences(current_user)
next if !preferences
objects_in_order_hash[preferences[:prio]] = object
objects_in_order_hash[preferences[:prio]] = local_class
end
objects_in_order_hash.keys.sort.reverse_each do |prio|
objects_in_order.push objects_in_order_hash[prio]
@ -64,16 +65,18 @@ class SearchController < ApplicationController
items = SearchIndexBackend.search(query, limit, objects_with_direct_search_index)
items.each do |item|
require item[:type].to_filename
record = Kernel.const_get(item[:type]).lookup(id: item[:id])
local_class = Kernel.const_get(item[:type])
record = local_class.lookup(id: item[:id])
next if !record
assets = record.assets(assets)
item[:type] = local_class.to_app_model.to_s
result.push item
end
end
# e. g. do ticket query by Ticket class to handle ticket permissions
objects_without_direct_search_index.each do |object|
object_result = search_generic_backend(object, query, limit, current_user, assets)
object_result = search_generic_backend(object.constantize, query, limit, current_user, assets)
if object_result.present?
result = result.concat(object_result)
end
@ -83,7 +86,7 @@ class SearchController < ApplicationController
result_in_order = []
objects_in_order.each do |object|
result.each do |item|
next if item[:type] != object
next if item[:type] != object.to_app_model.to_s
item[:id] = item[:id].to_i
result_in_order.push item
end
@ -110,7 +113,7 @@ class SearchController < ApplicationController
private
def search_generic_backend(object, query, limit, current_user, assets)
found_objects = object.constantize.search(
found_objects = object.search(
query: query,
limit: limit,
current_user: current_user,
@ -119,7 +122,7 @@ class SearchController < ApplicationController
found_objects.each do |found_object|
item = {
id: found_object.id,
type: found_object.class.to_s
type: found_object.class.to_app_model.to_s
}
result.push item
assets = found_object.assets(assets)

View file

@ -1,4 +1,15 @@
class Chat::Session < ApplicationModel
include HasSearchIndexBackend
include HasTags
extend Chat::Session::Search
load 'chat/session/search_index.rb'
include Chat::Session::SearchIndex
load 'chat/session/assets.rb'
include Chat::Session::Assets
has_many :messages, class_name: 'Chat::Message', foreign_key: 'chat_session_id'
before_create :generate_session_id
store :preferences

View file

@ -0,0 +1,56 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
module Chat::Session::Assets
=begin
get all assets / related models for this chat
chat = Chat::Session.find(123)
result = Chat::Session.assets(assets_if_exists)
returns
result = {
users: {
123: user_model_123,
1234: user_model_1234,
},
chat_sessions: [ chat_session_model1 ]
}
=end
def assets(data)
app_model_chat_session = Chat::Session.to_app_model
app_model_chat = Chat.to_app_model
app_model_user = User.to_app_model
data[ app_model_chat_session ] ||= {}
if !data[ app_model_chat_session ][ id ]
data[ app_model_chat_session ][ id ] = attributes_with_association_ids
data[ app_model_chat_session ][ id ]['messages'] = []
messages.each do |message|
data[ app_model_chat_session ][ id ]['messages'].push message.attributes
end
data[ app_model_chat_session ][ id ]['tags'] = tag_list
end
if !data[ app_model_chat ] || !data[ app_model_chat ][ chat_id ]
chat = Chat.lookup(id: chat_id)
if chat
data = chat.assets(data)
end
end
%w[created_by_id updated_by_id].each do |local_user_id|
next if !self[ local_user_id ]
next if data[ app_model_user ] && data[ app_model_user ][ self[ local_user_id ] ]
user = User.lookup(id: self[ local_user_id ])
next if !user
data = user.assets(data)
end
data
end
end

View file

@ -0,0 +1,80 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Chat::Session
module Search
=begin
search organizations preferences
result = Chat::Session.search_preferences(user_model)
returns if user has permissions to search
result = {
prio: 1000,
direct_search_index: true
}
returns if user has no permissions to search
result = false
=end
def search_preferences(current_user)
return false if Setting.get('chat') != true || !current_user.permissions?('chat.agent')
{
prio: 900,
direct_search_index: true,
}
end
=begin
search organizations
result = Chat::Session.search(
current_user: User.find(123),
query: 'search something',
limit: 15,
)
returns
result = [organization_model1, organization_model2]
=end
def search(params)
# get params
query = params[:query]
limit = params[:limit] || 10
current_user = params[:current_user]
# enable search only for agents and admins
return [] if !search_preferences(current_user)
# try search index backend
if SearchIndexBackend.enabled?
items = SearchIndexBackend.search(query, limit, 'Chat::Session')
chat_sessions = []
items.each do |item|
chat_session = Chat::Session.lookup(id: item[:id])
next if !chat_session
chat_sessions.push chat_session
end
return chat_sessions
end
# fallback do sql query
# - stip out * we already search for *query* -
query.delete! '*'
chat_sessions = Chat::Session.where(
'name LIKE ?', "%#{query}%"
).order('name').limit(limit).to_a
chat_sessions
end
end
end

View file

@ -0,0 +1,36 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
module Chat::Session::SearchIndex
=begin
lookup name of ref. objects
chat_session = Chat::Session.find(123)
result = chat_session.search_index_attribute_lookup
returns
attributes # object with lookup data
=end
def search_index_attribute_lookup
attributes = super
return if !attributes
attributes[:tag] = tag_list
messages = Chat::Message.where(chat_session_id: id)
attributes['messages'] = []
messages.each do |message|
# lookup attributes of ref. objects (normally name and note)
message_attributes = message.search_index_attribute_lookup
attributes['messages'].push message_attributes
end
attributes
end
end

View file

@ -49,7 +49,7 @@ class Sessions::Event::Base
true
end
def permission_check(key, event)
def current_user_id
if !@session
error = {
event: "#{event}_error",
@ -60,7 +60,7 @@ class Sessions::Event::Base
Sessions.send(@client_id, error)
return
end
if !@session['id']
if @session['id'].blank?
error = {
event: "#{event}_error",
data: {
@ -70,7 +70,13 @@ class Sessions::Event::Base
Sessions.send(@client_id, error)
return
end
user = User.lookup(id: @session['id'])
@session['id']
end
def current_user
user_id = current_user_id
return if !user_id
user = User.find_by(id: user_id)
if !user
error = {
event: "#{event}_error",
@ -81,6 +87,12 @@ class Sessions::Event::Base
Sessions.send(@client_id, error)
return
end
user
end
def permission_check(key, event)
user = current_user
return if !user
if !user.permissions?(key)
error = {
event: "#{event}_error",

View file

@ -0,0 +1,35 @@
class Sessions::Event::ChatSessionUpdate < Sessions::Event::ChatBase
def run
return super if super
return if !check_chat_session_exists
return if !permission_check('chat.agent', 'chat')
chat_session = current_chat_session
if @payload['data']['name'] != chat_session.name
chat_session.name = @payload['data']['name']
chat_session.save!
end
if @payload['data']['tags']
new_tags = @payload['data']['tags'].split(',')
new_tags.each(&:strip!)
tags = chat_session.tag_list
new_tags.each do |new_tag|
next if new_tag.blank?
next if tags.include?(new_tag)
chat_session.tag_add(new_tag, current_user_id)
end
tags.each do |tag|
next if new_tags.include?(tag)
chat_session.tag_remove(tag, current_user_id)
end
end
nil
end
end

View file

@ -243,8 +243,8 @@ class ModelTest < ActiveSupport::TestCase
assert(searchable.include?(Ticket))
assert(searchable.include?(User))
assert(searchable.include?(Organization))
assert_equal(3, searchable.count)
assert(searchable.include?(Chat::Session))
assert_equal(4, searchable.count)
end
end