From 59872f579a44b37cbeac8206e4737f3b60057abd Mon Sep 17 00:00:00 2001 From: Darren Clarke Date: Mon, 9 Feb 2026 22:18:35 +0100 Subject: [PATCH] Signal notification fixes and UI updates --- .../controllers/_profile/notification.coffee | 220 ++++++++++++++++++ .../_profile/signal_notifications.coffee | 89 ------- .../_ui_element/notification_matrix.coffee | 15 ++ .../mixins/ticket_notification_matrix.coffee | 29 +++ .../views/generic/notification_matrix.jst.eco | 70 ++++++ .../app/views/profile/notification.jst.eco | 86 +++++++ .../profile/signal_notifications.jst.eco | 86 ------- .../src/app/jobs/signal_notification_job.rb | 14 +- .../models/transaction/signal_notification.rb | 13 +- ...000001_add_signal_notification_settings.rb | 15 +- .../src/lib/signal_notification_sender.rb | 4 +- 11 files changed, 440 insertions(+), 201 deletions(-) create mode 100644 packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_profile/notification.coffee delete mode 100644 packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_profile/signal_notifications.coffee create mode 100644 packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_ui_element/notification_matrix.coffee create mode 100644 packages/zammad-addon-bridge/src/app/assets/javascripts/app/lib/mixins/ticket_notification_matrix.coffee create mode 100644 packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/generic/notification_matrix.jst.eco create mode 100644 packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/profile/notification.jst.eco delete mode 100644 packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/profile/signal_notifications.jst.eco diff --git a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_profile/notification.coffee b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_profile/notification.coffee new file mode 100644 index 0000000..8a2c7f6 --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_profile/notification.coffee @@ -0,0 +1,220 @@ +class ProfileNotification extends App.ControllerSubContent + @include App.TicketNotificationMatrix + + @requiredPermission: 'user_preferences.notifications+ticket.agent' + header: __('Notifications') + events: + 'submit form': 'update' + 'click .js-reset' : 'reset' + 'change .js-notificationSound': 'previewSound' + 'change #profile-groups-limit': 'didSwitchGroupsLimit' + 'change input[name=group_ids]': 'didChangeGroupIds' + 'change input[name$=".channel.signal"]': 'didChangeSignalCheckbox' + + elements: + '#profile-groups-limit': 'profileGroupsLimitInput' + '.profile-groups-limit-settings-inner': 'groupsLimitSettings' + '.profile-groups-all-unchecked': 'groupsAllUncheckedWarning' + + sounds: [ + { + name: 'Bell' + file: 'Bell.mp3' + }, + { + name: 'Kalimba' + file: 'Kalimba.mp3' + }, + { + name: 'Marimba' + file: 'Marimba.mp3' + }, + { + name: 'Peep' + file: 'Peep.mp3' + }, + { + name: 'Plop' + file: 'Plop.mp3' + }, + { + name: 'Ring' + file: 'Ring.mp3' + }, + { + name: 'Space' + file: 'Space.mp3' + }, + { + name: 'Wood' + file: 'Wood.mp3' + }, + { + name: 'Xylo' + file: 'Xylo.mp3' + } + ] + + constructor: -> + super + App.User.full(App.Session.get().id, @render, true, true) + + render: => + + matrix = + create: + name: __('New Ticket') + update: + name: __('Ticket update') + reminder_reached: + name: __('Ticket reminder reached') + escalation: + name: __('Ticket escalation') + + config = + group_ids: [] + matrix: {} + + user_config = @Session.get('preferences').notification_config + if user_config + config = $.extend(true, {}, config, user_config) + + # groups + user_group_config = true + if !user_config || !user_config['group_ids'] || _.isEmpty(user_config['group_ids']) || user_config['group_ids'][0] is '-' + user_group_config = false + + groups = [] + group_ids = App.User.find(@Session.get('id')).allGroupIds() + if group_ids + for group_id in group_ids + group = App.Group.find(group_id) + groups.push group + if !user_group_config + if !config['group_ids'] + config['group_ids'] = [] + config['group_ids'].push group_id.toString() + + groups = _.sortBy(groups, (item) -> return item.name) + + for sound in @sounds + sound.selected = sound.file is App.OnlineNotification.soundFile() ? true : false + + signal_notification_enabled = App.Config.get('signal_notification_enabled') + + signal_uid = config.signal_uid || '' + + # Check if any signal checkbox is currently checked in the matrix + signal_has_checked = false + if signal_notification_enabled + for key, val of config.matrix + if val?.channel?.signal + signal_has_checked = true + break + + @html App.view('profile/notification') + matrixTableHTML: @renderNotificationMatrix(config.matrix) + groups: groups + config: config + sounds: @sounds + notificationSoundEnabled: App.OnlineNotification.soundEnabled() + user_group_config: user_group_config + signal_notification_enabled: signal_notification_enabled + signal_uid: signal_uid + signal_has_checked: signal_has_checked + + update: (e) => + + #notification_config + e.preventDefault() + params = {} + params.notification_config = {} + + formParams = @formParam(e.target) + + params.notification_config.matrix = @updatedNotificationMatrixValues(formParams) + + if formParams.signal_uid? + params.notification_config.signal_uid = formParams.signal_uid + + if @profileGroupsLimitInput.is(':checked') + params.notification_config.group_ids = formParams['group_ids'] + if typeof params.notification_config.group_ids isnt 'object' + params.notification_config.group_ids = [params.notification_config.group_ids] + + if _.isEmpty(params.notification_config.group_ids) + delete params.notification_config.group_ids + + @formDisable(e) + + params.notification_sound = formParams.notification_sound + if !params.notification_sound.enabled + params.notification_sound.enabled = false + else + params.notification_sound.enabled = true + + # get data + @ajax( + id: 'preferences' + type: 'PUT' + url: @apiPath + '/users/preferences' + data: JSON.stringify(params) + processData: true + success: @success + error: @error + ) + + reset: (e) => + new App.ControllerConfirm( + message: __('Are you sure? Your notifications settings will be reset to default.') + buttonClass: 'btn--danger' + callback: => + @ajax( + id: 'preferences_notifications_reset' + type: 'POST' + url: "#{@apiPath}/users/preferences_notifications_reset" + processData: true + success: @success + ) + container: @el.closest('.content') + ) + + + success: (data, status, xhr) => + App.User.full( + App.Session.get('id'), + => + App.Event.trigger('ui:rerender') + @notify( + type: 'success' + msg: __('Update successful.') + ) + , + true + ) + + error: (xhr, status, error) => + @render() + data = JSON.parse(xhr.responseText) + @notify( + type: 'error' + msg: data.message + ) + + previewSound: (e) => + params = @formParam(e.target) + return if !params.notification_sound + return if !params.notification_sound.file + App.OnlineNotification.play(params.notification_sound.file) + + didSwitchGroupsLimit: (e) => + @groupsLimitSettings.collapse('toggle') + + didChangeGroupIds: (e) => + @groupsAllUncheckedWarning.toggleClass 'hide', @el.find('input[name=group_ids]:checked').length != 0 + + didChangeSignalCheckbox: (e) => + hasChecked = @el.find('input[name$=".channel.signal"]:checked').length > 0 + @el.find('.js-signal-phone-container').toggle(hasChecked) + +App.Config.set('Notifications', { prio: 2600, name: __('Notifications'), parent: '#profile', target: '#profile/notifications', permission: ['user_preferences.notifications+ticket.agent'], controller: ProfileNotification }, 'NavBarProfile') diff --git a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_profile/signal_notifications.coffee b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_profile/signal_notifications.coffee deleted file mode 100644 index 23d0df7..0000000 --- a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_profile/signal_notifications.coffee +++ /dev/null @@ -1,89 +0,0 @@ -class ProfileSignalNotifications extends App.ControllerSubContent - @requiredPermission: 'user_preferences.signal_notifications+ticket.agent' - header: __('Signal Notifications') - events: - 'submit form': 'update' - - constructor: -> - super - App.User.full(App.Session.get().id, @render, true, true) - - render: => - config = - enabled: false - events: - create: true - update: true - escalation: true - reminder_reached: true - - user = App.User.find(App.Session.get().id) - user_config = user.preferences?.signal_notifications - if user_config - config = $.extend(true, {}, config, user_config) - - @html App.view('profile/signal_notifications') - config: config - signal_uid: user.signal_uid || '' - signal_notification_enabled: App.Config.get('signal_notification_enabled') - - update: (e) => - e.preventDefault() - params = @formParam(e.target) - - preferences = {} - preferences.signal_notifications = - enabled: params.enabled == 'true' - events: - create: params.event_create == 'true' - update: params.event_update == 'true' - escalation: params.event_escalation == 'true' - reminder_reached: params.event_reminder_reached == 'true' - - @formDisable(e) - - @ajax( - id: 'preferences_signal_notifications' - type: 'PUT' - url: @apiPath + '/users/preferences' - data: JSON.stringify(preferences) - processData: true - success: @successPreferences - error: @error - ) - - if params.signal_uid? - user = App.User.find(App.Session.get().id) - user.signal_uid = params.signal_uid - user.save( - done: => - # User saved successfully - fail: (settings, details) => - @notify( - type: 'error' - msg: details.error || __('Failed to save Signal phone number') - ) - ) - - successPreferences: (data, status, xhr) => - App.User.full( - App.Session.get('id'), - => - App.Event.trigger('ui:rerender') - @notify( - type: 'success' - msg: __('Update successful.') - ) - , - true - ) - - error: (xhr, status, error) => - @render() - data = JSON.parse(xhr.responseText) - @notify( - type: 'error' - msg: data.message - ) - -App.Config.set('SignalNotifications', { prio: 2650, name: __('Signal Notifications'), parent: '#profile', target: '#profile/signal_notifications', permission: ['user_preferences.signal_notifications+ticket.agent'], controller: ProfileSignalNotifications }, 'NavBarProfile') diff --git a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_ui_element/notification_matrix.coffee b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_ui_element/notification_matrix.coffee new file mode 100644 index 0000000..e7550c1 --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/_ui_element/notification_matrix.coffee @@ -0,0 +1,15 @@ +# coffeelint: disable=camel_case_classes +class App.UiElement.notification_matrix + @render: (values, options = {}) -> + + matrixYAxe = + create: + name: __('New Ticket') + update: + name: __('Ticket update') + reminder_reached: + name: __('Ticket reminder reached') + escalation: + name: __('Ticket escalation') + + $( App.view('generic/notification_matrix')( matrixYAxe: matrixYAxe, values: values, signal_notification_enabled: options.signal_notification_enabled ) ) diff --git a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/lib/mixins/ticket_notification_matrix.coffee b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/lib/mixins/ticket_notification_matrix.coffee new file mode 100644 index 0000000..407d897 --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/lib/mixins/ticket_notification_matrix.coffee @@ -0,0 +1,29 @@ +# Common handling for the notification matrix +App.TicketNotificationMatrix = + renderNotificationMatrix: (values) -> + App.UiElement.notification_matrix.render(values, signal_notification_enabled: App.Config.get('signal_notification_enabled'))[0].outerHTML + + updatedNotificationMatrixValues: (formParams) -> + matrix = {} + + for key, value of formParams + area = key.split('.') + + continue if area[0] isnt 'matrix' + + if !matrix[area[1]] + matrix[area[1]] = {} + + switch area[2] + when 'criteria' + if !matrix[area[1]][area[2]] + matrix[area[1]][area[2]] = {} + + matrix[area[1]][area[2]][area[3]] = value is 'true' + when 'channel' + if !matrix[area[1]][area[2]] + matrix[area[1]][area[2]] = { online: true } + + matrix[area[1]][area[2]][area[3]] = value is 'true' + + matrix diff --git a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/generic/notification_matrix.jst.eco b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/generic/notification_matrix.jst.eco new file mode 100644 index 0000000..49c9e1a --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/generic/notification_matrix.jst.eco @@ -0,0 +1,70 @@ +<% if @signal_notification_enabled: %> + <% colWidth = "13%" %> + <% channelWidth = "100px" %> +<% else: %> + <% colWidth = "16%" %> + <% channelWidth = "120px" %> +<% end %> + + + + + + <% if @matrixYAxe: %> + <% for key, value of @matrixYAxe: %> + + +
+ <%- @T('My Tickets') %> + <%- @T('Not Assigned') %>* + <%- @T('Subscribed Tickets') %> + <%- @T('All Tickets') %>* + <%- @T('Also notify via email') %> + <% if @signal_notification_enabled: %> + <%- @T('Also notify via Signal') %> + <% end %> +
+ <%- @T(value.name) %> + <% criteria = @values[key]?.criteria %> + <% channel = @values[key]?.channel %> + + + + + + + + + + + <% if @signal_notification_enabled: %> + + + <% end %> + <% end %> + <% end %> +
diff --git a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/profile/notification.jst.eco b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/profile/notification.jst.eco new file mode 100644 index 0000000..65e43f9 --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/profile/notification.jst.eco @@ -0,0 +1,86 @@ + + +
+ +
+ <%- @matrixTableHTML %> +
+ + <% if @signal_notification_enabled: %> +
+

<%- @T('Signal Phone Number') %>

+
+ +

<%- @T('Use international format with country code (e.g., +1234567890)') %>

+
+
+ <% end %> + + <% if @groups: %> +
+ checked <% end %>> + +
+

+ <%- @T('Limit Groups') %> +

+ +
+
+ + + + + + + + <% for group in @groups: %> + + +
<%- @T('Group') %> + <%- @T('Not Assigned') %> & <%- @T('All Tickets') %> +
<%- @P(group, 'name') %> + + + <% end %> +
+
+
+ <% end %> + +

<%- @T('Sounds') %>

+
+
+ +
+
+ + <%- @Icon('arrow-down') %> +
+
+
+ +
+ + + +
diff --git a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/profile/signal_notifications.jst.eco b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/profile/signal_notifications.jst.eco deleted file mode 100644 index 4899934..0000000 --- a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/views/profile/signal_notifications.jst.eco +++ /dev/null @@ -1,86 +0,0 @@ - - -<% if !@signal_notification_enabled: %> - -<% end %> - -
-

<%- @T('Signal Phone Number') %>

-

- <%- @T('Enter your Signal phone number to receive ticket notifications via Signal.') %> -

- -
- - -

<%- @T('Use international format with country code (e.g., +1234567890)') %>

-
- -

<%- @T('Notification Settings') %>

- -
- -
- -

<%- @T('Notification Events') %>

-

- <%- @T('Select which events should trigger Signal notifications.') %> -

- -
- -
- -
- -
- -
- -
- -
- -
- - -
diff --git a/packages/zammad-addon-bridge/src/app/jobs/signal_notification_job.rb b/packages/zammad-addon-bridge/src/app/jobs/signal_notification_job.rb index 7c23a3c..c960196 100644 --- a/packages/zammad-addon-bridge/src/app/jobs/signal_notification_job.rb +++ b/packages/zammad-addon-bridge/src/app/jobs/signal_notification_job.rb @@ -11,7 +11,9 @@ class SignalNotificationJob < ApplicationJob user = User.find_by(id: user_id) return if !user - return if user.signal_uid.blank? + + signal_uid = user.preferences.dig('notification_config', 'signal_uid').presence + return if signal_uid.blank? article = article_id ? Ticket::Article.find_by(id: article_id) : nil @@ -30,13 +32,13 @@ class SignalNotificationJob < ApplicationJob SignalNotificationSender.send_message( channel: channel, - recipient: user.signal_uid, + recipient: signal_uid, message: message ) - add_history(ticket, user, type) + add_history(ticket, user, signal_uid, type) - Rails.logger.info "Sent Signal notification to #{user.signal_uid} for ticket ##{ticket.number} (#{type})" + Rails.logger.info "Sent Signal notification to #{signal_uid} for ticket ##{ticket.number} (#{type})" end private @@ -48,8 +50,8 @@ class SignalNotificationJob < ApplicationJob Channel.find_by(id: channel_id, area: 'Signal::Number', active: true) end - def add_history(ticket, user, type) - identifier = user.signal_uid.presence || user.login + def add_history(ticket, user, signal_uid, type) + identifier = signal_uid.presence || user.login recipient_list = "#{identifier}(#{type}:signal)" History.add( diff --git a/packages/zammad-addon-bridge/src/app/models/transaction/signal_notification.rb b/packages/zammad-addon-bridge/src/app/models/transaction/signal_notification.rb index 415c441..5cb2af0 100644 --- a/packages/zammad-addon-bridge/src/app/models/transaction/signal_notification.rb +++ b/packages/zammad-addon-bridge/src/app/models/transaction/signal_notification.rb @@ -91,9 +91,8 @@ class Transaction::SignalNotification possible_recipients_with_ooo.each do |user| next if recipient_is_current_user?(user) next if !user.active? - next if !user_has_signal_notifications_enabled?(user) - next if user.signal_uid.blank? - next if !should_notify_for_event?(user) + next if user_signal_uid(user).blank? + next if !user_wants_signal_for_event?(user) recipients.push(user) end @@ -123,11 +122,11 @@ class Transaction::SignalNotification false end - def user_has_signal_notifications_enabled?(user) - user.preferences.dig('signal_notifications', 'enabled') == true + def user_signal_uid(user) + user.preferences.dig('notification_config', 'signal_uid').presence end - def should_notify_for_event?(user) + def user_wants_signal_for_event?(user) event_type = @item[:type] return false if event_type.blank? @@ -139,6 +138,6 @@ class Transaction::SignalNotification else return false end - user.preferences.dig('signal_notifications', 'events', event_key) == true + user.preferences.dig('notification_config', 'matrix', event_key, 'channel', 'signal') == true end end diff --git a/packages/zammad-addon-bridge/src/db/addon/bridge/20260120000001_add_signal_notification_settings.rb b/packages/zammad-addon-bridge/src/db/addon/bridge/20260120000001_add_signal_notification_settings.rb index 9e1bf23..e894ae2 100644 --- a/packages/zammad-addon-bridge/src/db/addon/bridge/20260120000001_add_signal_notification_settings.rb +++ b/packages/zammad-addon-bridge/src/db/addon/bridge/20260120000001_add_signal_notification_settings.rb @@ -57,20 +57,13 @@ class AddSignalNotificationSettings < ActiveRecord::Migration[5.2] frontend: false ) - # Permission for Signal notifications profile page - Permission.create_if_not_exists( - name: 'user_preferences.signal_notifications', - description: 'Manage Signal notification preferences', - preferences: { - translations: ['Profile - Signal Notifications'] - } - ) end def self.down + # Only destroy the transaction backend registration. + # Preserve signal_notification_enabled and signal_notification_channel_id + # so admin configuration survives addon reinstalls (setup.rb runs + # uninstall + install on every container start). Setting.find_by(name: '0105_signal_notification')&.destroy - Setting.find_by(name: 'signal_notification_enabled')&.destroy - Setting.find_by(name: 'signal_notification_channel_id')&.destroy - Permission.find_by(name: 'user_preferences.signal_notifications')&.destroy end end diff --git a/packages/zammad-addon-bridge/src/lib/signal_notification_sender.rb b/packages/zammad-addon-bridge/src/lib/signal_notification_sender.rb index 8528892..861a6c8 100644 --- a/packages/zammad-addon-bridge/src/lib/signal_notification_sender.rb +++ b/packages/zammad-addon-bridge/src/lib/signal_notification_sender.rb @@ -23,8 +23,8 @@ class SignalNotificationSender return if recipient.blank? return if message.blank? - api_url = channel.options[:api_url] - api_token = channel.options[:api_token] + api_url = channel.options['bot_endpoint'] || channel.options[:bot_endpoint] + api_token = channel.options['bot_token'] || channel.options[:bot_token] return if api_url.blank? || api_token.blank?