diff --git a/app/assets/javascripts/app/controllers/chat.coffee b/app/assets/javascripts/app/controllers/chat.coffee index 2966e3b4c..8eb974089 100644 --- a/app/assets/javascripts/app/controllers/chat.coffee +++ b/app/assets/javascripts/app/controllers/chat.coffee @@ -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') diff --git a/app/assets/javascripts/app/controllers/search.coffee b/app/assets/javascripts/app/controllers/search.coffee index 9a6f45052..67e9c009a 100644 --- a/app/assets/javascripts/app/controllers/search.coffee +++ b/app/assets/javascripts/app/controllers/search.coffee @@ -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 diff --git a/app/assets/javascripts/app/models/chat_sessions.coffee b/app/assets/javascripts/app/models/chat_sessions.coffee new file mode 100644 index 000000000..d32fccd18 --- /dev/null +++ b/app/assets/javascripts/app/models/chat_sessions.coffee @@ -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' diff --git a/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco b/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco index 1fef2ed9b..de2f271da 100644 --- a/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco +++ b/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco @@ -7,9 +7,7 @@
- <%= @name %>
-
<%- @Icon('info') %>
-
+ <%= @name %> #<%= @session.id %>
<%- @T('disconnect') %>
@@ -24,6 +22,25 @@
+
diff --git a/app/assets/javascripts/app/views/customer_chat/chat_window_info.jst.eco b/app/assets/javascripts/app/views/customer_chat/chat_window_info.jst.eco deleted file mode 100644 index a7c3b1ff3..000000000 --- a/app/assets/javascripts/app/views/customer_chat/chat_window_info.jst.eco +++ /dev/null @@ -1,17 +0,0 @@ -
- \ No newline at end of file diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 1ec9c76f8..fad61f23c 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -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) diff --git a/app/models/chat/session.rb b/app/models/chat/session.rb index 70cd0d818..cf83beea4 100644 --- a/app/models/chat/session.rb +++ b/app/models/chat/session.rb @@ -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 diff --git a/app/models/chat/session/assets.rb b/app/models/chat/session/assets.rb new file mode 100644 index 000000000..6410e2692 --- /dev/null +++ b/app/models/chat/session/assets.rb @@ -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 diff --git a/app/models/chat/session/search.rb b/app/models/chat/session/search.rb new file mode 100644 index 000000000..87d12f1c9 --- /dev/null +++ b/app/models/chat/session/search.rb @@ -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 diff --git a/app/models/chat/session/search_index.rb b/app/models/chat/session/search_index.rb new file mode 100644 index 000000000..b81196722 --- /dev/null +++ b/app/models/chat/session/search_index.rb @@ -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 diff --git a/lib/sessions/event/base.rb b/lib/sessions/event/base.rb index 5c1b659f2..1b02e6bfe 100644 --- a/lib/sessions/event/base.rb +++ b/lib/sessions/event/base.rb @@ -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", diff --git a/lib/sessions/event/chat_session_update.rb b/lib/sessions/event/chat_session_update.rb new file mode 100644 index 000000000..0f93dfc31 --- /dev/null +++ b/lib/sessions/event/chat_session_update.rb @@ -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 diff --git a/test/unit/model_test.rb b/test/unit/model_test.rb index 9c5431539..d31e69fbb 100644 --- a/test/unit/model_test.rb +++ b/test/unit/model_test.rb @@ -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