hyrax 2.0.0.beta1 → 2.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
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|