Added desktop notifications.

This commit is contained in:
Martin Edenhofer 2015-12-08 00:51:33 +01:00
parent a28dc17b32
commit 84d30e4ee0
11 changed files with 124 additions and 46 deletions

View file

@ -550,6 +550,25 @@ class App.Controller extends Spine.Controller
'meta-task-update' 'meta-task-update'
) )
locationVerify: (e, callback) =>
newLocation = $(e.currentTarget).attr 'href'
@log 'debug', "new location #{newLocation}"
return if !newLocation
currentLocation = Spine.Route.getPath()
@log 'debug', "current location #{currentLocation}"
return if newLocation.replace(/#/, '') isnt currentLocation
@locationExecute(newLocation, callback)
locationExecute: (location, callback) =>
if callback
callback()
location = location.replace(/#/, '')
@log 'debug', "execute controller again for '#{location}' because of same hash"
Spine.Route.matchRoutes(location)
logoUrl: ->
"#{@Config.get('image_path')}/#{@Config.get('product_logo')}"
class App.ControllerPermanent extends App.Controller class App.ControllerPermanent extends App.Controller
constructor: -> constructor: ->
super super

View file

@ -150,11 +150,9 @@ class App.SettingsAreaLogo extends App.Controller
@render() @render()
render: -> render: ->
logoFile = App.Config.get('product_logo')
logoUrl = App.Config.get('image_path') + "/#{logoFile}"
@html App.view('settings/logo')( @html App.view('settings/logo')(
setting: @setting setting: @setting
logoUrl: logoUrl logoUrl: @logoUrl()
) )
onLogoPick: (event) => onLogoPick: (event) =>

View file

@ -94,6 +94,10 @@ class App.CustomerChat extends App.Controller
# do not play sound on initial load # do not play sound on initial load
if counter > 0 && @lastWaitingChatCount isnt undefined if counter > 0 && @lastWaitingChatCount isnt undefined
@sounds.chat_new.play() @sounds.chat_new.play()
@notifyDesktop(
title: "#{counter} #{App.i18n.translateInline('Waiting Customers')}",
url: '#customer_chat'
)
@lastWaitingChatCount = counter @lastWaitingChatCount = counter
# collect chat window messages # collect chat window messages
@ -152,7 +156,6 @@ class App.CustomerChat extends App.Controller
addChat: (session) -> addChat: (session) ->
return if @chatWindows[session.session_id] return if @chatWindows[session.session_id]
chat = new ChatWindow chat = new ChatWindow
name: "#{session.created_at}"
session: session session: session
removeCallback: @removeChat removeCallback: @removeChat
messageCallback: @updateNavMenu messageCallback: @updateNavMenu
@ -234,6 +237,9 @@ class ChatWindow extends App.Controller
@isAgentTyping = false @isAgentTyping = false
@resetUnreadMessages() @resetUnreadMessages()
chat = App.Chat.find(@session.chat_id)
@name = "#{chat.displayName()} [##{@session.id}]"
@on 'layout-change', @scrollToBottom @on 'layout-change', @scrollToBottom
@bind('chat_session_typing', (data) => @bind('chat_session_typing', (data) =>
@ -252,10 +258,14 @@ class ChatWindow extends App.Controller
@addStatusMessage("<strong>#{data.realname}</strong> has left the conversation") @addStatusMessage("<strong>#{data.realname}</strong> has left the conversation")
@goOffline() @goOffline()
) )
@bind('chat_focus', (data) =>
return if data.session_id isnt @session.session_id
@focus()
)
render: -> render: ->
@html App.view('customer_chat/chat_window') @html App.view('customer_chat/chat_window')
name: @options.name name: @name
@el.one 'transitionend', @onTransitionend @el.one 'transitionend', @onTransitionend
@ -389,6 +399,13 @@ class ChatWindow extends App.Controller
@addUnreadMessages() @addUnreadMessages()
@updateModified(true) @updateModified(true)
@sounds.message.play() @sounds.message.play()
@notifyDesktop(
title: @name
body: message
url: '#customer_chat'
callback: =>
App.Event.trigger('chat_focus', { session_id: @session.session_id })
)
unreadMessages: => unreadMessages: =>
@unreadMessagesCounter @unreadMessagesCounter

View file

@ -305,13 +305,10 @@ class Base extends App.Wizard
else else
url = "#{http_type}://#{fqdn}" url = "#{http_type}://#{fqdn}"
logoFile = App.Config.get('product_logo')
logoUrl = App.Config.get('image_path') + "/#{logoFile}"
organization = App.Config.get('organization') organization = App.Config.get('organization')
@html App.view('getting_started/base')( @html App.view('getting_started/base')(
url: url url: url
logoUrl: logoUrl logoUrl: @logoUrl()
organization: organization organization: organization
) )
@$('input, select').first().focus() @$('input, select').first().focus()

View file

@ -53,12 +53,9 @@ class Index extends App.ControllerContent
if @Config.get( provider.config ) is true || @Config.get( provider.config ) is 'true' if @Config.get( provider.config ) is true || @Config.get( provider.config ) is 'true'
auth_providers.push provider auth_providers.push provider
logoFile = App.Config.get('product_logo')
logoUrl = App.Config.get('image_path') + "/#{logoFile}"
@html App.view('login')( @html App.view('login')(
item: data item: data
logoUrl: logoUrl logoUrl: @logoUrl()
auth_providers: auth_providers auth_providers: auth_providers
) )

View file

@ -1,6 +1,7 @@
class App.TaskbarWidget extends App.Controller class App.TaskbarWidget extends App.Controller
events: events:
'click .js-close': 'remove' 'click .js-close': 'remove'
'click .js-locationVerify': 'location'
constructor: -> constructor: ->
super super
@ -74,6 +75,10 @@ class App.TaskbarWidget extends App.Controller
@el.sortable(dndOptions) @el.sortable(dndOptions)
location: (e) =>
return if !$(e.currentTarget).hasClass('is-modified')
@locationVerify(e)
remove: (e, key = false, force = false) => remove: (e, key = false, force = false) =>
e.preventDefault() e.preventDefault()
if !key if !key

View file

@ -1,4 +1,7 @@
class App.Notify extends App.ControllerWidgetPermanent class App.Notify extends App.ControllerWidgetPermanent
desktopNotify: {}
desktopNotifyCounter: 0
events: events:
'click .alert': 'destroy' 'click .alert': 'destroy'
@ -12,17 +15,48 @@ class App.Notify extends App.ControllerWidgetPermanent
@log 'notify:removeall', @ @log 'notify:removeall', @
@destroyAll() @destroyAll()
@bind 'notifyDesktop', (data) -> @bind 'notifyDesktop', (data) =>
return if !window.Notification
if !data['icon'] if !data['icon']
data['icon'] = 'unknown' data['icon'] = @logoUrl()
notify.createNotification( data.msg, data )
timeout = 60000 * 60 * 24
if document.hasFocus()
timeout = 4000
@desktopNotifyCounter += 1
counter = @desktopNotifyCounter
notification = new window.Notification(data.title, data)
@desktopNotify[counter] = notification
notification.onclose = (e) =>
delete @desktopNotify[counter]
notification.onclick = (e) =>
window.focus()
if data.url
@locationExecute(data.url)
if data.callback
data.callback()
if data.timeout || timeout
App.Delay.set(
-> notification.close()
data.timeout || timeout
)
# request desktop notification after login # request desktop notification after login
@bind 'auth', (data) -> @bind 'auth', (data) ->
if !_.isEmpty(data) if !_.isEmpty(data)
notify.requestPermission() return if !window.Notification
window.Notification.requestPermission()
notify.config( pageVisibility: false ) $(window).focus(
=>
for counter, notification of @desktopNotify
notification.close()
)
render: (data) -> render: (data) ->
@ -53,4 +87,4 @@ class App.Notify extends App.ControllerWidgetPermanent
destroyAll: -> destroyAll: ->
$.noty.closeAll() $.noty.closeAll()
App.Config.set( 'notify', App.Notify, 'Widgets' ) App.Config.set('notify', App.Notify, 'Widgets')

View file

@ -1,4 +1,6 @@
class App.OnlineNotificationWidget extends App.Controller class App.OnlineNotificationWidget extends App.Controller
alreadyShown: {}
elements: elements:
'.js-toggleNotifications': 'toggle' '.js-toggleNotifications': 'toggle'
@ -102,6 +104,7 @@ class App.OnlineNotificationWidget extends App.Controller
fetch: => fetch: =>
load = (items) => load = (items) =>
@fetchedData = true
App.OnlineNotification.refresh( items, { clear: true } ) App.OnlineNotification.refresh( items, { clear: true } )
@updateContent() @updateContent()
App.OnlineNotification.fetchFull(load) App.OnlineNotification.fetchFull(load)
@ -133,17 +136,21 @@ class App.OnlineNotificationWidget extends App.Controller
notificationsContainer = $('.js-notificationsContainer .popover-content') notificationsContainer = $('.js-notificationsContainer .popover-content')
# generate desktop notifications
for item in items
if !@alreadyShown[item.id]
@alreadyShown[item.id] = true
if @fetchedData
word = "#{item.type}d"
title = "#{item.created_by.displayName()} #{App.i18n.translateInline(word)} #{App.i18n.translateInline(item.object_name)} #{item.title}"
@notifyDesktop(
url: item.link
title: title
)
# execute controller again of already open (because hash hasn't changed, we need to do it manually) # execute controller again of already open (because hash hasn't changed, we need to do it manually)
notificationsContainer.find('.js-locationVerify').on('click', (e) => notificationsContainer.find('.js-locationVerify').on('click', (e) =>
newLocation = $(e.target).attr 'href' @locationVerify(e, @hidePopover)
if !newLocation
newLocation = $(e.target).closest('.js-locationVerify').attr 'href'
return if !newLocation
currentLocation = Spine.Route.getPath()
return if newLocation.replace(/#/, '') isnt currentLocation
@hidePopover()
@log 'debug', "execute controller again for '#{currentLocation}' because of same hash"
Spine.Route.matchRoutes(currentLocation)
) )
# close notification list on click # close notification list on click

View file

@ -1,5 +1,5 @@
<% for item in @taskItems: %> <% for item in @taskItems: %>
<a href="<%- item.meta.url %>" title="<%= item.meta.title %>" class="nav-tab task <%= item.meta.class %><% if item.task.active: %> is-active<% end %><% if item.task.notify: %> is-modified<% end %>" data-key="<%- item.task.key %>"> <a href="<%- item.meta.url %>" title="<%= item.meta.title %>" class="nav-tab task js-locationVerify <%= item.meta.class %><% if item.task.active: %> is-active<% end %><% if item.task.notify: %> is-modified<% end %>" data-key="<%- item.task.key %>">
<div class="nav-tab-icon" title="<%- @Ti(item.meta.iconTitle) %>"> <div class="nav-tab-icon" title="<%- @Ti(item.meta.iconTitle) %>">
<% if item.meta.type is 'task': %> <% if item.meta.type is 'task': %>
<% if item.task.notify: %> <% if item.task.notify: %>

View file

@ -7,7 +7,7 @@
<div class="activity-body"> <div class="activity-body">
<a class="activity-message js-locationVerify" href="<%- item.link %>"> <a class="activity-message js-locationVerify" href="<%- item.link %>">
<span class="activity-text"> <span class="activity-text">
<%= item.created_by.displayName() %> <%- @T( item.type ) %> <%- @T( item.object_name ) %><% if item.title: %> <strong><%= item.title %></strong><% end %> <%= item.created_by.displayName() %> <%- @T( "#{item.type}d" ) %> <%- @T( item.object_name ) %><% if item.title: %> <strong><%= item.title %></strong><% end %>
</span> </span>
<%- @humanTime(item.created_at, false, 'activity-time') %> <%- @humanTime(item.created_at, false, 'activity-time') %>
</a> </a>

View file

@ -502,13 +502,28 @@ class ChatTest < TestCase
end end
def test_timeouts def test_timeouts
agent = browser_instance
login(
browser: agent,
username: 'master@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all(
browser: agent,
)
click(
browser: agent,
css: 'a[href="#customer_chat"]',
)
agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click)
# no customer action, hide widget
customer = browser_instance customer = browser_instance
location( location(
browser: customer, browser: customer,
url: "#{browser_url}/assets/chat/znuny.html?port=#{ENV['WS_PORT']}", url: "#{browser_url}/assets/chat/znuny.html?port=#{ENV['WS_PORT']}",
) )
# no customer action, hide widget
watch_for( watch_for(
browser: customer, browser: customer,
css: '.zammad-chat', css: '.zammad-chat',
@ -557,22 +572,11 @@ class ChatTest < TestCase
browser: customer, browser: customer,
css: '.js-chat-open', css: '.js-chat-open',
) )
watch_for(
agent = browser_instance
login(
browser: agent, browser: agent,
username: 'master@example.com', css: '.js-chatMenuItem .counter',
password: 'test', value: '1',
url: browser_url,
) )
tasks_close_all(
browser: agent,
)
click(
browser: agent,
css: 'a[href="#customer_chat"]',
)
agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click)
click( click(
browser: agent, browser: agent,
css: '.active .js-acceptChat', css: '.active .js-acceptChat',