Added desktop notifications.
This commit is contained in:
parent
a28dc17b32
commit
84d30e4ee0
11 changed files with 124 additions and 46 deletions
|
@ -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
|
||||||
|
|
|
@ -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) =>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: %>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue