hyrax 2.0.0.beta1 → 2.0.0.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -2
  3. data/app/actors/hyrax/actors/file_set_actor.rb +12 -15
  4. data/app/assets/javascripts/hyrax.js +6 -5
  5. data/app/assets/javascripts/hyrax/{app.js → app.js.erb} +12 -8
  6. data/app/assets/javascripts/hyrax/channels/notifications.js +0 -0
  7. data/app/assets/javascripts/hyrax/notification.es6 +19 -31
  8. data/app/channels/hyrax/application_cable/channel.rb +6 -0
  9. data/app/channels/hyrax/application_cable/connection.rb +30 -0
  10. data/app/channels/hyrax/notifications_channel.rb +15 -0
  11. data/app/controllers/hyrax/depositors_controller.rb +2 -2
  12. data/app/controllers/hyrax/notifications_controller.rb +2 -0
  13. data/app/controllers/hyrax/users_controller.rb +0 -7
  14. data/app/helpers/hyrax/hyrax_helper_behavior.rb +27 -0
  15. data/app/jobs/import_url_job.rb +3 -1
  16. data/app/jobs/stream_notifications_job.rb +10 -0
  17. data/app/models/proxy_deposit_request.rb +2 -2
  18. data/app/models/user_mailbox.rb +19 -0
  19. data/app/services/hyrax/{message_user_service.rb → abstract_message_service.rb} +7 -4
  20. data/app/services/hyrax/batch_create_failure_service.rb +1 -1
  21. data/app/services/hyrax/batch_create_success_service.rb +1 -1
  22. data/app/services/hyrax/fixity_check_failure_service.rb +1 -1
  23. data/app/services/hyrax/import_url_failure_service.rb +1 -1
  24. data/app/services/hyrax/messenger_service.rb +8 -0
  25. data/app/services/hyrax/workflow/abstract_notification.rb +1 -1
  26. data/app/views/_user_util_links.html.erb +1 -1
  27. data/app/views/hyrax/homepage/index.html.erb +1 -1
  28. data/config/locales/hyrax.de.yml +5 -2
  29. data/config/locales/hyrax.en.yml +4 -1
  30. data/config/locales/hyrax.es.yml +4 -1
  31. data/config/locales/hyrax.fr.yml +5 -2
  32. data/config/locales/hyrax.it.yml +4 -1
  33. data/config/locales/hyrax.pt-BR.yml +5 -2
  34. data/config/locales/hyrax.zh.yml +5 -2
  35. data/config/routes.rb +4 -2
  36. data/db/migrate/20170905135339_add_preferred_locale_to_users.rb +5 -0
  37. data/lib/generators/hyrax/templates/config/authorities/licenses.yml +28 -10
  38. data/lib/generators/hyrax/templates/config/hyrax.rb +0 -3
  39. data/lib/hyrax/configuration.rb +0 -5
  40. data/lib/hyrax/engine.rb +5 -0
  41. data/lib/hyrax/version.rb +1 -1
  42. data/spec/actors/hyrax/actors/file_set_actor_spec.rb +74 -0
  43. data/spec/channels/hyrax/application_cable/channel_spec.rb +14 -0
  44. data/spec/channels/hyrax/application_cable/connection_spec.rb +31 -0
  45. data/spec/channels/hyrax/notifications_channel_spec.rb +45 -0
  46. data/spec/controllers/hyrax/notifications_controller_spec.rb +1 -0
  47. data/spec/helpers/blacklight_helper_spec.rb +4 -2
  48. data/spec/helpers/hyrax_helper_spec.rb +34 -4
  49. data/spec/jobs/import_url_job_spec.rb +1 -1
  50. data/spec/jobs/stream_notifications_job_spec.rb +30 -0
  51. data/spec/models/collection_spec.rb +13 -19
  52. data/spec/models/proxy_deposit_request_spec.rb +2 -1
  53. data/spec/models/user_mailbox_spec.rb +73 -0
  54. data/spec/services/hyrax/abstract_message_service_spec.rb +25 -0
  55. data/spec/services/hyrax/messenger_service_spec.rb +15 -0
  56. data/spec/test_app_templates/lib/generators/test_app_generator.rb +6 -0
  57. data/spec/views/_user_util_links.html.erb_spec.rb +3 -2
  58. data/spec/views/hyrax/homepage/index.html.erb_spec.rb +4 -0
  59. data/template.rb +1 -1
  60. metadata +23 -14
  61. data/app/assets/javascripts/hyrax/notifications.es6 +0 -63
  62. data/app/views/hyrax/users/_notify_number.html.erb +0 -8
  63. data/app/views/hyrax/users/notifications_number.json.jbuilder +0 -1
  64. data/app/views/kaminari/blacklight/_first_page.html.erb +0 -9
  65. data/app/views/kaminari/blacklight/_gap.html.erb +0 -8
  66. data/app/views/kaminari/blacklight/_last_page.html.erb +0 -9
  67. data/app/views/kaminari/blacklight/_next_page.html.erb +0 -9
  68. data/app/views/kaminari/blacklight/_page.html.erb +0 -10
  69. data/app/views/kaminari/blacklight/_paginator.html.erb +0 -19
  70. data/app/views/kaminari/blacklight/_prev_page.html.erb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5939c7dfed45843b17856611c64ffc82ee4cd492
4
- data.tar.gz: d21b1da8507c09373ac8d9b3e0075df873301bad
3
+ metadata.gz: b1cd321b1dc4e68407d13358c574ce8fe0a0d5c9
4
+ data.tar.gz: 92530f677f3c9cedb6d6c88446437cf9742596a1
5
5
  SHA512:
6
- metadata.gz: 742c2367eaf3138530f3bf644003664e86e63b20b8a4ccd5905197fc0ee9da9aa7bdb7f6b72f9a991fdbf7d4f434a9d5d215a83de258ac661dd0dbe251fc1b32
7
- data.tar.gz: 819f25b0076a067f2dce5e621ead98d4f2511e61cc0f1a2756a9034512564023772016779e46edeef48f1f787e63c20a5f06f631fc3b3a3669f509d1a7cf88ec
6
+ metadata.gz: a7aad9a39bc1ff847e9e40ddcdbe7c3eebd24a115ba5967b84cf1482e9225f87f29435a65a19ad4c10f39b4fe4401612ad92dc2d30a42ae4da5df249291c4574
7
+ data.tar.gz: 740acb400efae6271442fdca1b88506f86cfccbbd6efff94c2a2e1641c78d3fad59d79841c527f2393dd1478632c8ce1098d33c0aa978167e333d94801695434
data/README.md CHANGED
@@ -36,6 +36,7 @@ Jump in: [![Slack Status](http://slack.samvera.org/badge.svg)](http://slack.samv
36
36
  * [Start background workers](#start-background-workers)
37
37
  * [Create default administrative set](#create-default-administrative-set)
38
38
  * [Generate a work type](#generate-a-work-type)
39
+ * [Enable notifications](#enable-notifications)
39
40
  * [Managing a Hyrax\-based app](#managing-a-hyrax-based-app)
40
41
  * [Toggling features](#toggling-features)
41
42
  * [License](#license)
@@ -62,7 +63,7 @@ The Samvera community is here to help. Please see our [support guide](./.github/
62
63
  # Getting started
63
64
 
64
65
  This document contains instructions specific to setting up an app with __Hyrax
65
- v2.0.0.alpha__. If you are looking for instructions on installing a different
66
+ v2.0.0.beta2__. If you are looking for instructions on installing a different
66
67
  version, be sure to select the appropriate branch or tag from the drop-down
67
68
  menu above.
68
69
 
@@ -159,7 +160,7 @@ NOTE: The steps need to be done in order to create a new Hyrax based app.
159
160
  Generate a new Rails application using the template.
160
161
 
161
162
  ```
162
- rails _5.0.5_ new my_app -m https://raw.githubusercontent.com/samvera/hyrax/v2.0.0.beta1/template.rb
163
+ rails _5.0.5_ new my_app -m https://raw.githubusercontent.com/samvera/hyrax/v2.0.0.beta2/template.rb
163
164
  ```
164
165
 
165
166
  Generating a new Rails application using Hyrax's template above takes cares of a number of steps for you, including:
@@ -244,6 +245,16 @@ rails generate hyrax:work My/MovingImage
244
245
 
245
246
  You may wish to [customize your work type](https://github.com/samvera/hyrax/wiki/Customizing-your-work-types) now that it's been generated.
246
247
 
248
+ ## Enable notifications
249
+
250
+ Hyrax 2 uses a WebSocket-based user notifications system, which requires Redis. To enable user notifications, make sure that you have configured ActionCable to use Redis as the adapter in your application's `config/cable.yml`. E.g., for the `development` Rails environment:
251
+
252
+ ``` yaml
253
+ development:
254
+ adapter: redis
255
+ url: redis://localhost:6379
256
+ ```
257
+
247
258
  # Managing a Hyrax-based app
248
259
 
249
260
  The [Hyrax Management Guide](https://github.com/samvera/hyrax/wiki/Hyrax-Management-Guide) provides tips for how to manage, customize, and enhance your Hyrax application, including guidance specific to:
@@ -12,17 +12,25 @@ module Hyrax
12
12
 
13
13
  # @!group Asynchronous Operations
14
14
 
15
- # Spawns asynchronous IngestJob
16
- # Called from FileSetsController, AttachFilesToWorkJob, ImportURLJob, IngestLocalFileJob
15
+ # Spawns asynchronous IngestJob unless ingesting from URL
16
+ # Called from FileSetsController, AttachFilesToWorkJob, IngestLocalFileJob, ImportUrlJob
17
17
  # @param [Hyrax::UploadedFile, File, ActionDigest::HTTP::UploadedFile] file the file uploaded by the user
18
18
  # @param [Symbol, #to_s] relation
19
19
  # @return [IngestJob, FalseClass] false on failure, otherwise the queued job
20
- def create_content(file, relation = :original_file)
20
+ def create_content(file, relation = :original_file, from_url: false)
21
21
  # If the file set doesn't have a title or label assigned, set a default.
22
22
  file_set.label ||= label_for(file)
23
23
  file_set.title = [file_set.label] if file_set.title.blank?
24
24
  return false unless file_set.save # Need to save to get an id
25
- IngestJob.perform_later(wrapper!(file: file, relation: relation))
25
+ if from_url
26
+ # If ingesting from URL, don't spawn an IngestJob; instead
27
+ # reach into the FileActor and run the ingest with the file instance in
28
+ # hand. Do this because we don't have the underlying UploadedFile instance
29
+ file_actor = build_file_actor(relation)
30
+ file_actor.ingest_file(wrapper!(file: file, relation: relation))
31
+ else
32
+ IngestJob.perform_later(wrapper!(file: file, relation: relation))
33
+ end
26
34
  end
27
35
 
28
36
  # Spawns asynchronous IngestJob with user notification afterward
@@ -33,17 +41,6 @@ module Hyrax
33
41
  IngestJob.perform_later(wrapper!(file: file, relation: relation), notification: true)
34
42
  end
35
43
 
36
- # Spawns async ImportUrlJob to attach remote file to fileset
37
- # @param [#to_s] url
38
- # @return [IngestUrlJob] the queued job
39
- # @todo Remove as it appears to be untested and not called
40
- def import_url(url)
41
- file_set.update(import_url: url.to_s)
42
- operation = Hyrax::Operation.create!(user: user, operation_type: "Attach File")
43
- ImportUrlJob.perform_later(file_set, operation)
44
- end
45
- deprecation_deprecate import_url: "appears to be untested and not used, and will be removed unless an issue/PR is submitted verifying otherwise"
46
-
47
44
  # @!endgroup
48
45
 
49
46
  # Adds the appropriate metadata, visibility and relationships to file_set
@@ -37,11 +37,16 @@
37
37
  //= require webcomponentsjs/0.5.4/CustomElements.min
38
38
  //= require time-elements
39
39
 
40
+ //= require action_cable
41
+
40
42
  //= require hyrax/monkey_patch_turbolinks
43
+ //= require hyrax/fileupload
44
+ // Provide AMD module support
45
+ //= require almond
46
+ //= require hyrax/notification
41
47
  //= require hyrax/app
42
48
  //= require hyrax/config
43
49
  //= require hyrax/initialize
44
- //= require hyrax/fileupload
45
50
  //= require hyrax/trophy
46
51
  //= require hyrax/facets
47
52
  //= require hyrax/featured_works
@@ -59,8 +64,6 @@
59
64
  //= require hyrax/dashboard_actions
60
65
  //= require hyrax/batch
61
66
  //= require hyrax/flot_stats
62
- // Provide AMD module support
63
- //= require almond
64
67
  //= require hyrax/admin/admin_set_controls
65
68
  //= require hyrax/admin/admin_set/group_participants
66
69
  //= require hyrax/admin/admin_set/registered_users
@@ -73,8 +76,6 @@
73
76
  //= require hyrax/admin/graphs
74
77
  //= require hyrax/save_work
75
78
  //= require hyrax/permissions
76
- //= require hyrax/notification
77
- //= require hyrax/notifications
78
79
  //= require hyrax/autocomplete
79
80
  //= require hyrax/autocomplete/default
80
81
  //= require hyrax/autocomplete/work
@@ -40,7 +40,6 @@ Hyrax = {
40
40
  var controls = new CollectionControls($('#collection-controls'));
41
41
  },
42
42
 
43
-
44
43
  // Pretty graphs on the dashboard page
45
44
  adminStatisticsGraphs: function() {
46
45
  var AdminGraphs = require('hyrax/admin/graphs');
@@ -87,13 +86,18 @@ Hyrax = {
87
86
  new PermissionsControl($("#collection_permissions"), 'tmpl-collection-grant');
88
87
  },
89
88
 
90
- // Polling for user notifications. This is displayed in the navbar.
91
- notifications: function () {
92
- var Notifications = require('hyrax/notifications');
93
- $('[data-update-poll-url]').each(function () {
94
- var interval = $(this).data('update-poll-interval');
95
- var url = $(this).data('update-poll-url');
96
- new Notifications(url, interval);
89
+ // ActionCable for user notifications. This is displayed in the navbar.
90
+ notifications: function() {
91
+ var consumer = ActionCable.createConsumer("<%= Hyrax::Engine.routes.url_helpers.notifications_endpoint_path %>");
92
+ consumer.subscriptions.create("Hyrax::NotificationsChannel", {
93
+ connected: function(data) {
94
+ this.perform("update_locale", { locale: $('html').attr('lang') });
95
+ },
96
+
97
+ received: function(data) {
98
+ var Notification = require('hyrax/notification');
99
+ new Notification($('.notify-number')).update(data.notifications_count, data.notifications_label);
100
+ }
97
101
  });
98
102
  },
99
103
 
@@ -1,37 +1,25 @@
1
- // This is the notification widget on the page
2
-
3
- export default class {
4
- constructor(dom_obj) {
5
- this.dom_obj = dom_obj;
6
- this.counter = dom_obj.find('.count');
1
+ export default class Notification {
2
+ /**
3
+ * Initializes the notification widget on the page and allows
4
+ * updating of the notification count and notification label
5
+ *
6
+ * @param {jQuery} element the notification widget
7
+ */
8
+ constructor(element) {
9
+ this.element = element
10
+ this.counter = element.find('.count')
7
11
  }
8
12
 
9
- setCount(count) {
10
- this.counter.html(count);
11
- if (count == 0) {
12
- this.noNotifications()
13
- } else {
14
- this.hasNotifications(count)
15
- }
16
- }
13
+ update(count, label) {
14
+ this.element.attr('aria-label', label)
15
+ this.counter.html(count)
17
16
 
18
- // set the styles for no unread notifications
19
- noNotifications () {
17
+ if (count === 0) {
20
18
  this.counter.addClass('invisible')
21
- this.dom_obj.prop('aria-label', this.notificationsLabel(0))
22
- }
23
-
24
- // set the styles for having unread notifications
25
- hasNotifications (size) {
26
- this.counter.removeClass('invisible')
27
- this.dom_obj.prop('aria-label', this.notificationsLabel(size))
28
- }
29
-
30
- notificationsLabel(size) {
31
- if (size == 0)
32
- return "You have no unread notifications"
33
- if (size == 1)
34
- return "You have one unread notification"
35
- return `You have %{size} unread notifications`
19
+ }
20
+ else {
21
+ this.counter.removeClass('invisible')
22
+ this.counter.addClass('label-danger').removeClass('label-default')
23
+ }
36
24
  }
37
25
  }
@@ -0,0 +1,6 @@
1
+ module Hyrax
2
+ module ApplicationCable
3
+ class Channel < ActionCable::Channel::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,30 @@
1
+ module Hyrax
2
+ module ApplicationCable
3
+ class Connection < ActionCable::Connection::Base
4
+ identified_by :current_user
5
+
6
+ def connect
7
+ self.current_user = find_verified_user
8
+ end
9
+
10
+ private
11
+
12
+ def find_verified_user
13
+ user = ::User.find_by(id: user_id)
14
+ if user
15
+ user
16
+ else
17
+ reject_unauthorized_connection
18
+ end
19
+ end
20
+
21
+ def user_id
22
+ session['warden.user.user.key'][0][0]
23
+ end
24
+
25
+ def session
26
+ cookies.encrypted[Rails.application.config.session_options[:key]]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ module Hyrax
2
+ class NotificationsChannel < ApplicationCable::Channel
3
+ def subscribed
4
+ stream_for current_user
5
+ end
6
+
7
+ def unsubscribed
8
+ stop_all_streams
9
+ end
10
+
11
+ def update_locale(data)
12
+ current_user.update(preferred_locale: data['locale'])
13
+ end
14
+ end
15
+ end
@@ -38,8 +38,8 @@ module Hyrax
38
38
  def send_proxy_depositor_added_messages(grantor, grantee)
39
39
  message_to_grantee = "#{grantor.name} has assigned you as a proxy depositor"
40
40
  message_to_grantor = "You have assigned #{grantee.name} as a proxy depositor"
41
- ::User.batch_user.send_message(grantor, message_to_grantor, "Proxy Depositor Added")
42
- ::User.batch_user.send_message(grantee, message_to_grantee, "Proxy Depositor Added")
41
+ Hyrax::MessengerService.deliver(::User.batch_user, grantor, message_to_grantor, "Proxy Depositor Added")
42
+ Hyrax::MessengerService.deliver(::User.batch_user, grantee, message_to_grantee, "Proxy Depositor Added")
43
43
  end
44
44
  end
45
45
  end
@@ -8,6 +8,8 @@ module Hyrax
8
8
  add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
9
9
  add_breadcrumb t(:'hyrax.admin.sidebar.notifications'), hyrax.notifications_path
10
10
  @messages = user_mailbox.inbox
11
+ # Update the notifications now that there are zero unread
12
+ StreamNotificationsJob.perform_later(current_user)
11
13
  end
12
14
 
13
15
  def delete_all
@@ -16,13 +16,6 @@ module Hyrax
16
16
  @presenter = Hyrax::UserProfilePresenter.new(user, current_ability)
17
17
  end
18
18
 
19
- def notifications_number
20
- @notify_number = 0
21
- return if action_name == "index" && controller_name == "mailbox"
22
- return unless user_signed_in?
23
- @notify_number = current_user.mailbox.inbox(unread: true).count
24
- end
25
-
26
19
  private
27
20
 
28
21
  # TODO: this should move to a service.
@@ -54,6 +54,23 @@ module Hyrax
54
54
  user_agent.include? 'Chrome'
55
55
  end
56
56
 
57
+ # @param [User] user
58
+ def render_notifications(user:)
59
+ mailbox = UserMailbox.new(user)
60
+ unread_notifications = mailbox.unread_count
61
+ link_to(hyrax.notifications_path,
62
+ 'aria-label' => mailbox.label(params[:locale]),
63
+ class: 'notify-number') do
64
+ capture do
65
+ concat content_tag(:span, '', class: 'fa fa-bell')
66
+ concat "\n"
67
+ concat content_tag(:span,
68
+ unread_notifications,
69
+ class: count_classes_for(unread_notifications))
70
+ end
71
+ end
72
+ end
73
+
57
74
  # @param [ProxyDepositRequest] req
58
75
  def show_transfer_request_title(req)
59
76
  if req.deleted_work? || req.canceled?
@@ -233,6 +250,16 @@ module Hyrax
233
250
  request.user_agent || ''
234
251
  end
235
252
 
253
+ def count_classes_for(unread_count)
254
+ 'count label '.tap do |classes|
255
+ classes << if unread_count.zero?
256
+ 'invisible label-default'
257
+ else
258
+ 'label-danger'
259
+ end
260
+ end
261
+ end
262
+
236
263
  # rubocop:disable Metrics/MethodLength
237
264
  def search_action_for_dashboard
238
265
  case params[:controller]
@@ -20,6 +20,8 @@ class ImportUrlJob < Hyrax::ApplicationJob
20
20
  operation.performing!
21
21
  user = User.find_by_user_key(file_set.depositor)
22
22
  uri = URI(file_set.import_url)
23
+ # @todo Use Hydra::Works::AddExternalFileToFileSet instead of manually
24
+ # copying the file here. This will be gnarly.
23
25
  copy_remote_file(uri) do |f|
24
26
  # reload the FileSet once the data is copied since this is a long running task
25
27
  file_set.reload
@@ -27,7 +29,7 @@ class ImportUrlJob < Hyrax::ApplicationJob
27
29
  # FileSetActor operates synchronously so that this tempfile is available.
28
30
  # If asynchronous, the job might be invoked on a machine that did not have this temp file on its file system!
29
31
  # NOTE: The return status may be successful even if the content never attaches.
30
- if Hyrax::Actors::FileSetActor.new(file_set, user).create_content(f)
32
+ if Hyrax::Actors::FileSetActor.new(file_set, user).create_content(f, from_url: true)
31
33
  operation.success!
32
34
  else
33
35
  # send message to user on download failure
@@ -0,0 +1,10 @@
1
+ class StreamNotificationsJob < Hyrax::ApplicationJob
2
+ def perform(users)
3
+ Array.wrap(users).each do |user|
4
+ mailbox = UserMailbox.new(user)
5
+ Hyrax::NotificationsChannel.broadcast_to(user,
6
+ notifications_count: mailbox.unread_count,
7
+ notifications_label: mailbox.label)
8
+ end
9
+ end
10
+ end
@@ -90,13 +90,13 @@ class ProxyDepositRequest < ActiveRecord::Base
90
90
  user_link = link_to(sending_user.name, Hyrax::Engine.routes.url_helpers.user_path(sending_user.user_key))
91
91
  transfer_link = link_to('transfer requests', Hyrax::Engine.routes.url_helpers.transfers_path)
92
92
  message = "#{user_link} wants to transfer a work to you. Review all #{transfer_link}"
93
- User.batch_user.send_message(receiving_user, message, "Ownership Change Request")
93
+ Hyrax::MessengerService.deliver(::User.batch_user, receiving_user, message, "Ownership Change Request")
94
94
  end
95
95
 
96
96
  def send_request_transfer_message_as_part_of_update
97
97
  message = "Your transfer request was #{status}."
98
98
  message += " Comments: #{receiver_comment}" if receiver_comment.present?
99
- User.batch_user.send_message(sending_user, message, "Ownership Change #{status}")
99
+ Hyrax::MessengerService.deliver(::User.batch_user, sending_user, message, "Ownership Change #{status}")
100
100
  end
101
101
 
102
102
  public
@@ -10,6 +10,21 @@ class UserMailbox
10
10
  messages.each { |m| m.mark_as_read(user) }
11
11
  end
12
12
 
13
+ def unread_count
14
+ user.mailbox.inbox(unread: true).count
15
+ end
16
+
17
+ def label(locale_from_params = nil)
18
+ case unread_count
19
+ when 0
20
+ I18n.t("hyrax.toolbar.notifications.zero", locale: locale_from_params || preferred_locale)
21
+ when 1
22
+ I18n.t("hyrax.toolbar.notifications.one", locale: locale_from_params || preferred_locale)
23
+ else
24
+ I18n.t("hyrax.toolbar.notifications.many", count: unread_count, locale: locale_from_params || preferred_locale)
25
+ end
26
+ end
27
+
13
28
  def delete_all
14
29
  user.mailbox.inbox.each do |msg|
15
30
  delete_message(msg)
@@ -32,6 +47,10 @@ class UserMailbox
32
47
  msg.move_to_trash(msg.participants[1])
33
48
  end
34
49
 
50
+ def preferred_locale
51
+ user.preferred_locale || I18n.default_locale
52
+ end
53
+
35
54
  def empty_trash(user)
36
55
  user.mailbox.trash.each do |conv|
37
56
  conv.messages.each do |notify|