thredded 0.4.0 → 0.5.0

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.mkdn +26 -0
  3. data/README.mkdn +52 -7
  4. data/app/assets/images/thredded/moderation.svg +4 -0
  5. data/app/assets/images/thredded/private-messages.svg +1 -1
  6. data/app/assets/images/thredded/settings.svg +1 -1
  7. data/app/assets/stylesheets/thredded/_base.scss +1 -1
  8. data/app/assets/stylesheets/thredded/_thredded.scss +1 -0
  9. data/app/assets/stylesheets/thredded/base/_buttons.scss +16 -0
  10. data/app/assets/stylesheets/thredded/base/_grid.scss +12 -0
  11. data/app/assets/stylesheets/thredded/components/_base.scss +4 -0
  12. data/app/assets/stylesheets/thredded/components/_messageboard.scss +1 -13
  13. data/app/assets/stylesheets/thredded/components/_topic-header.scss +27 -0
  14. data/app/assets/stylesheets/thredded/layout/_main-navigation.scss +1 -1
  15. data/app/assets/stylesheets/thredded/layout/_moderation.scss +45 -0
  16. data/app/assets/stylesheets/thredded/layout/_navigation.scss +20 -6
  17. data/app/assets/stylesheets/thredded/layout/_search-navigation.scss +2 -2
  18. data/app/assets/stylesheets/thredded/layout/_user-navigation.scss +5 -3
  19. data/app/commands/thredded/autofollow_mentioned_users.rb +32 -0
  20. data/app/commands/thredded/moderate_post.rb +35 -0
  21. data/app/commands/thredded/notify_following_users.rb +18 -0
  22. data/app/commands/thredded/notify_private_topic_users.rb +4 -4
  23. data/app/controllers/thredded/application_controller.rb +6 -25
  24. data/app/controllers/thredded/messageboards_controller.rb +5 -3
  25. data/app/controllers/thredded/moderation_controller.rb +56 -0
  26. data/app/controllers/thredded/post_permalinks_controller.rb +1 -1
  27. data/app/controllers/thredded/posts_controller.rb +4 -2
  28. data/app/controllers/thredded/private_post_permalinks_controller.rb +1 -1
  29. data/app/controllers/thredded/private_topics_controller.rb +2 -3
  30. data/app/controllers/thredded/theme_previews_controller.rb +3 -3
  31. data/app/controllers/thredded/topics_controller.rb +32 -11
  32. data/app/forms/thredded/topic_form.rb +1 -0
  33. data/app/helpers/thredded/application_helper.rb +26 -1
  34. data/app/helpers/thredded/urls_helper.rb +7 -5
  35. data/app/jobs/thredded/auto_follow_and_notify_job.rb +13 -0
  36. data/app/jobs/thredded/notify_private_topic_users_job.rb +3 -4
  37. data/app/mailer_previews/thredded/post_mailer_preview.rb +2 -2
  38. data/app/mailers/thredded/post_mailer.rb +2 -2
  39. data/app/models/concerns/thredded/content_moderation_state.rb +53 -0
  40. data/app/models/concerns/thredded/moderation_state.rb +13 -0
  41. data/app/models/concerns/thredded/post_common.rb +6 -71
  42. data/app/models/concerns/thredded/topic_common.rb +26 -11
  43. data/app/models/concerns/thredded/user_topic_read_state_common.rb +4 -0
  44. data/app/models/thredded/messageboard.rb +2 -0
  45. data/app/models/thredded/post.rb +24 -0
  46. data/app/models/thredded/post_moderation_record.rb +45 -0
  47. data/app/models/thredded/private_post.rb +15 -0
  48. data/app/models/thredded/private_topic.rb +8 -0
  49. data/app/models/thredded/topic.rb +39 -0
  50. data/app/models/thredded/user_detail.rb +11 -0
  51. data/app/models/thredded/user_extender.rb +14 -0
  52. data/app/models/thredded/user_topic_follow.rb +20 -0
  53. data/app/policies/thredded/messageboard_policy.rb +15 -0
  54. data/app/policies/thredded/post_policy.rb +17 -1
  55. data/app/policies/thredded/private_post_policy.rb +1 -1
  56. data/app/policies/thredded/private_topic_policy.rb +1 -1
  57. data/app/policies/thredded/topic_policy.rb +18 -1
  58. data/app/view_models/thredded/base_topic_view.rb +0 -13
  59. data/app/view_models/thredded/post_view.rb +8 -1
  60. data/app/view_models/thredded/posts_page_view.rb +6 -8
  61. data/app/view_models/thredded/private_topic_view.rb +8 -0
  62. data/app/view_models/thredded/private_topics_page_view.rb +24 -0
  63. data/app/view_models/thredded/topic_posts_page_view.rb +17 -0
  64. data/app/view_models/thredded/topic_view.rb +27 -1
  65. data/app/view_models/thredded/topics_page_view.rb +2 -4
  66. data/app/views/thredded/moderation/_post.html.erb +7 -0
  67. data/app/views/thredded/moderation/_post_moderation_actions.html.erb +12 -0
  68. data/app/views/thredded/moderation/_post_moderation_record.html.erb +43 -0
  69. data/app/views/thredded/moderation/history.html.erb +19 -0
  70. data/app/views/thredded/moderation/pending.html.erb +29 -0
  71. data/app/views/thredded/post_mailer/{at_notification.html.erb → post_notification.html.erb} +1 -1
  72. data/app/views/thredded/post_mailer/{at_notification.text.erb → post_notification.text.erb} +1 -1
  73. data/app/views/thredded/posts/_post.html.erb +8 -1
  74. data/app/views/thredded/posts_common/_actions.html.erb +11 -0
  75. data/app/views/thredded/posts_common/_content.html.erb +5 -0
  76. data/app/views/thredded/posts_common/_header.html.erb +5 -0
  77. data/app/views/thredded/private_posts/_private_post.html.erb +5 -1
  78. data/app/views/thredded/shared/_nav.html.erb +1 -0
  79. data/app/views/thredded/shared/_page.html.erb +1 -1
  80. data/app/views/thredded/shared/nav/_moderation.html.erb +13 -0
  81. data/app/views/thredded/shared/nav/_private_topics.html.erb +1 -2
  82. data/app/views/thredded/topics/_header.html.erb +15 -0
  83. data/app/views/thredded/topics/index.html.erb +2 -2
  84. data/app/views/thredded/topics/new.html.erb +1 -1
  85. data/config/locales/en.yml +26 -5
  86. data/config/locales/pt-BR.yml +23 -0
  87. data/config/routes.rb +7 -0
  88. data/db/migrate/20160329231848_create_thredded.rb +40 -5
  89. data/db/seeds.rb +5 -5
  90. data/db/upgrade_migrations/20160429222452_upgrade_v0_3_to_v0_4.rb +1 -2
  91. data/db/upgrade_migrations/20160501151908_upgrade_v0_4_to_v0_5.rb +56 -0
  92. data/heroku.gemfile.lock +20 -18
  93. data/lib/generators/thredded/install/templates/initializer.rb +15 -0
  94. data/lib/html/pipeline/at_mention_filter.rb +5 -2
  95. data/lib/thredded.rb +4 -0
  96. data/lib/thredded/at_users.rb +3 -2
  97. data/lib/thredded/content_formatter.rb +81 -0
  98. data/lib/thredded/version.rb +1 -1
  99. metadata +28 -10
  100. data/app/commands/thredded/notify_mentioned_users.rb +0 -55
  101. data/app/jobs/thredded/at_notifier_job.rb +0 -12
  102. data/app/mailer_previews/thredded/private_post_mailer_preview.rb +0 -12
  103. data/app/mailers/thredded/private_post_mailer.rb +0 -17
  104. data/app/views/thredded/posts_common/_post.html.erb +0 -26
  105. data/app/views/thredded/private_post_mailer/at_notification.html.erb +0 -13
@@ -40,18 +40,20 @@ module Thredded
40
40
  end
41
41
 
42
42
  # @param post [Post, PrivatePost]
43
+ # @param user [Thredded.user_class] the current user
43
44
  # @return [String] URL of the topic page with the post anchor.
44
- def post_url(post, params = {})
45
+ def post_url(post, user:, **params)
45
46
  params = params.dup
46
47
  params[:anchor] ||= ActionView::RecordIdentifier.dom_id(post)
47
- params[:page] ||= post.page
48
+ params[:page] ||= post.private_topic_post? ? post.page : post.page(user: user)
48
49
  topic_url(post.postable, params)
49
50
  end
50
51
 
51
52
  # @param post [Post, PrivatePost]
53
+ # @param user [Thredded.user_class] the current user
52
54
  # @return [String] path to the topic page with the post anchor.
53
- def post_path(post, params = {})
54
- post_url(post, params.merge(only_path: true))
55
+ def post_path(post, user:, **params)
56
+ post_url(post, params.merge(user: user, only_path: true))
55
57
  end
56
58
 
57
59
  # @param post [Post, PrivatePost]
@@ -65,7 +67,7 @@ module Thredded
65
67
  end
66
68
 
67
69
  # @param post [Post, PrivatePost]
68
- # @return [String] path to the DELETE PATCH PUT POST endpoint.
70
+ # @return [String] path to the DELETE endpoint.
69
71
  def delete_post_path(post)
70
72
  if post.private_topic_post?
71
73
  private_topic_private_post_path(post.postable, post)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module Thredded
3
+ class AutoFollowAndNotifyJob < ::ActiveJob::Base
4
+ queue_as :default
5
+
6
+ def perform(post_id)
7
+ post = Post.find(post_id)
8
+
9
+ AutofollowMentionedUsers.new(post).run
10
+ NotifyFollowingUsers.new(post).run
11
+ end
12
+ end
13
+ end
@@ -3,10 +3,9 @@ module Thredded
3
3
  class NotifyPrivateTopicUsersJob < ::ActiveJob::Base
4
4
  queue_as :default
5
5
 
6
- def perform(private_topic_id)
7
- private_topic = Thredded::PrivateTopic.find(private_topic_id)
8
-
9
- NotifyPrivateTopicUsers.new(private_topic).run
6
+ def perform(private_post_id)
7
+ private_post = Thredded::PrivatePost.find(private_post_id)
8
+ NotifyPrivateTopicUsers.new(private_post).run
10
9
  end
11
10
  end
12
11
  end
@@ -2,8 +2,8 @@
2
2
  module Thredded
3
3
  # Previews for the PostMailer
4
4
  class PostMailerPreview < BaseMailerPreview
5
- def at_notification
6
- PostMailer.at_notification(
5
+ def post_notification
6
+ PostMailer.post_notification(
7
7
  mock_post(content: mock_content(mention_users: %w(glebm joel))),
8
8
  %w(glebm@test.com joel@test.com)
9
9
  )
@@ -2,10 +2,10 @@
2
2
  require_dependency 'thredded/topic_email_view'
3
3
  module Thredded
4
4
  class PostMailer < Thredded::BaseMailer
5
- def at_notification(post_id, emails)
5
+ def post_notification(post_id, emails)
6
6
  @post = find_record Post, post_id
7
7
  email_details = TopicEmailView.new(@post.postable)
8
- headers['X-SMTPAPI'] = email_details.smtp_api_tag('at_notification')
8
+ headers['X-SMTPAPI'] = email_details.smtp_api_tag('post_notification')
9
9
 
10
10
  mail from: email_details.no_reply,
11
11
  to: email_details.no_reply,
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ module Thredded
3
+ # Moderation state of a piece of content, such as a Topic or a Post.
4
+ # Requires an integer moderation_state column, a user_id column, and a user_detail association on the including class.
5
+ # @api private
6
+ module ContentModerationState
7
+ extend ActiveSupport::Concern
8
+ include ModerationState
9
+
10
+ included do
11
+ before_validation :set_default_moderation_state, on: :create
12
+
13
+ scope :moderation_state_visible_to_user, (lambda do |user|
14
+ # @type [Arel::Table]
15
+ table = arel_table
16
+ # @type [Arel::Nodes::Node]
17
+ visible_to_all =
18
+ if Thredded.content_visible_while_pending_moderation
19
+ table[:moderation_state].not_eq(moderation_states[:blocked])
20
+ else
21
+ table[:moderation_state].eq(moderation_states[:approved])
22
+ end
23
+ where(
24
+ if user && !user.thredded_anonymous?
25
+ visible_to_all.or(table[:user_id].eq(user.id))
26
+ else
27
+ visible_to_all
28
+ end
29
+ )
30
+ end)
31
+ end
32
+
33
+ # Whether this is visible to anyone based on the moderation state.
34
+ def moderation_state_visible_to_all?
35
+ if Thredded.content_visible_while_pending_moderation
36
+ !blocked?
37
+ else
38
+ approved?
39
+ end
40
+ end
41
+
42
+ # Whether this is visible to the given user based on the moderation state.
43
+ def moderation_state_visible_to_user?(user)
44
+ moderation_state_visible_to_all? || (!user.thredded_anonymous? && user_id == user.id)
45
+ end
46
+
47
+ private
48
+
49
+ def set_default_moderation_state
50
+ self.moderation_state ||= user_detail.moderation_state
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module Thredded
3
+ # Defines a moderation_state enum
4
+ # Requires an integer moderation_state column on the including class.
5
+ module ModerationState
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ enum moderation_state: %i(pending_moderation approved blocked)
10
+ validates :moderation_state, presence: true
11
+ end
12
+ end
13
+ end
@@ -1,42 +1,9 @@
1
1
  # frozen_string_literal: true
2
+ require_dependency 'thredded/content_formatter'
2
3
  module Thredded
3
4
  module PostCommon
4
5
  extend ActiveSupport::Concern
5
6
 
6
- WHITELIST_TRANSFORMERS = HTML::Pipeline::SanitizationFilter::WHITELIST[:transformers] + [
7
- lambda do |env|
8
- node = env[:node]
9
-
10
- a_tags = node.css('a')
11
- a_tags.each do |a_tag|
12
- if a_tag['href'].starts_with? 'http'
13
- a_tag['target'] = '_blank'
14
- a_tag['rel'] = 'nofollow noopener'
15
- end
16
- end
17
- end
18
- ].freeze
19
-
20
- WHITELIST_ELEMENTS = HTML::Pipeline::SanitizationFilter::WHITELIST[:elements] + %w(
21
- iframe span
22
- ).freeze
23
-
24
- WHITELIST = HTML::Pipeline::SanitizationFilter::WHITELIST.deep_merge(
25
- elements: WHITELIST_ELEMENTS,
26
- transformers: WHITELIST_TRANSFORMERS,
27
- attributes: {
28
- 'a' => %w(href rel),
29
- 'iframe' => %w(src width height frameborder allowfullscreen sandbox seamless),
30
- 'span' => %w(class),
31
- },
32
- add_attributes: {
33
- 'iframe' => {
34
- 'seamless' => 'seamless',
35
- 'sandbox' => 'allow-forms allow-scripts',
36
- }
37
- }
38
- ).freeze
39
-
40
7
  included do
41
8
  paginates_per 50
42
9
 
@@ -49,11 +16,6 @@ module Thredded
49
16
  scope :order_oldest_first, -> { order(id: :asc) }
50
17
 
51
18
  after_commit :update_parent_last_user_and_timestamp, on: [:create, :destroy]
52
- after_commit :notify_at_users, on: [:create, :update]
53
- end
54
-
55
- def page(per_page: self.class.default_per_page)
56
- 1 + postable.posts.where('id < ?', id).count / per_page
57
19
  end
58
20
 
59
21
  def avatar_url
@@ -61,35 +23,12 @@ module Thredded
61
23
  end
62
24
 
63
25
  # @param view_context [Object] the context of the rendering view.
26
+ # @return [String] formatted and sanitized html-safe post content.
64
27
  def filtered_content(view_context)
65
- pipeline = HTML::Pipeline.new(content_pipeline_filters, content_pipeline_options)
66
- result = pipeline.call(content, view_context: view_context)
67
- result[:output].to_s.html_safe
68
- end
69
-
70
- protected
71
-
72
- # @return [Array<HTML::Pipeline::Filter]>]
73
- def content_pipeline_filters
74
- [
75
- HTML::Pipeline::VimeoFilter,
76
- HTML::Pipeline::YoutubeFilter,
77
- HTML::Pipeline::BbcodeFilter,
78
- HTML::Pipeline::MarkdownFilter,
79
- HTML::Pipeline::SanitizationFilter,
80
- HTML::Pipeline::AtMentionFilter,
81
- HTML::Pipeline::EmojiFilter,
82
- HTML::Pipeline::AutolinkFilter,
83
- ]
84
- end
85
-
86
- # @return [Hash] options for HTML::Pipeline.new
87
- def content_pipeline_options
88
- {
89
- asset_root: Rails.application.config.action_controller.asset_host || '',
90
- post: self,
91
- whitelist: WHITELIST,
92
- }
28
+ Thredded::ContentFormatter.new(
29
+ view_context,
30
+ users_provider: -> (names) { readers_from_user_names(names) }
31
+ ).format_content(content)
93
32
  end
94
33
 
95
34
  private
@@ -103,9 +42,5 @@ module Thredded
103
42
  end
104
43
  postable.update!(last_user_id: last_post.user_id, updated_at: last_post.created_at)
105
44
  end
106
-
107
- def notify_at_users
108
- AtNotifierJob.perform_later(self.class.name, id)
109
- end
110
45
  end
111
46
  end
@@ -47,24 +47,39 @@ module Thredded
47
47
  .merge(reads_class.where(reads[:id].eq(nil).or(reads[:read_at].lt(topics[:updated_at]))))
48
48
  end
49
49
 
50
+ private
51
+
52
+ # @param user [Thredded.user_class]
53
+ # @return [ByPostableLookup]
54
+ def read_states_by_postable_hash(user)
55
+ read_states = reflect_on_association(:user_read_states).klass
56
+ .where(user_id: user.id, postable_id: current_scope.map(&:id))
57
+ Thredded::TopicCommon::CachingHash.from_relation(read_states)
58
+ end
59
+
60
+ public
61
+
50
62
  # @param user [Thredded.user_class]
51
63
  # @return [Array<[TopicCommon, UserTopicReadStateCommon]>]
52
64
  def with_read_states(user)
53
65
  null_read_state = Thredded::NullUserTopicReadState.new
54
66
  return current_scope.zip([null_read_state]) if user.thredded_anonymous?
55
- read_state_by_topic_id =
56
- reflect_on_association(:user_read_states).klass
57
- .where(user_id: user.id, postable_id: current_scope.map(&:id))
58
- .group_by(&:postable_id)
59
- current_scope.map do |topic|
60
- read_state = read_state_by_topic_id[topic.id]
61
- if read_state
62
- read_state = read_state[0]
63
- read_state.postable = topic
64
- end
65
- [topic, read_state || null_read_state]
67
+ read_states_by_postable = read_states_by_postable_hash(user)
68
+ current_scope.map do |postable|
69
+ [postable, read_states_by_postable[postable] || null_read_state]
66
70
  end
67
71
  end
68
72
  end
73
+
74
+ class CachingHash < Hash
75
+ def self.from_relation(postable_relation)
76
+ self[postable_relation.map { |related| [related.postable_id, related] }]
77
+ end
78
+
79
+ # lookup related item by postable and set the inverse lookup
80
+ def [](postable)
81
+ super(postable.id).tap { |related| related.postable = postable if related }
82
+ end
83
+ end
69
84
  end
70
85
  end
@@ -26,6 +26,10 @@ module Thredded
26
26
  return unless !state.read_at? || state.read_at < post.updated_at
27
27
  state.update!(read_at: post.updated_at, page: post_page)
28
28
  end
29
+
30
+ def read_on_first_post!(user, topic)
31
+ create!(user: user, postable: topic, read_at: Time.zone.now, page: 1)
32
+ end
29
33
  end
30
34
  end
31
35
  end
@@ -47,6 +47,8 @@ module Thredded
47
47
  foreign_key: :messageboard_group_id,
48
48
  class_name: 'Thredded::MessageboardGroup'
49
49
 
50
+ has_many :post_moderation_records, inverse_of: :messageboard, dependent: :delete_all
51
+
50
52
  default_scope { where(closed: false).order(topics_count: :desc) }
51
53
 
52
54
  scope :top_level_messageboards, -> { where(group: nil) }
@@ -2,6 +2,7 @@
2
2
  module Thredded
3
3
  class Post < ActiveRecord::Base
4
4
  include PostCommon
5
+ include ContentModerationState
5
6
 
6
7
  belongs_to :user,
7
8
  class_name: Thredded.user_class,
@@ -17,18 +18,41 @@ module Thredded
17
18
  primary_key: :user_id,
18
19
  foreign_key: :user_id,
19
20
  counter_cache: true
21
+ has_many :moderation_records,
22
+ class_name: 'Thredded::PostModerationRecord',
23
+ dependent: :nullify
20
24
 
21
25
  validates :messageboard_id, presence: true
22
26
 
27
+ after_commit :auto_follow_and_notify, on: [:create, :update]
28
+
29
+ # @param [Integer] per_page
30
+ # @param [Thredded.user_class] user
31
+ def page(per_page: self.class.default_per_page, user:)
32
+ readable_posts = PostPolicy::Scope.new(user, postable.posts).resolve
33
+ 1 + readable_posts.where(postable.posts.arel_table[:id].lt(id)).count / per_page
34
+ end
35
+
23
36
  def private_topic_post?
24
37
  false
25
38
  end
26
39
 
40
+ def user_detail
41
+ super || build_user_detail
42
+ end
43
+
27
44
  # @return [ActiveRecord::Relation<Thredded.user_class>] users from the list of user names that can read this post.
28
45
  def readers_from_user_names(user_names)
29
46
  DbTextSearch::CaseInsensitive
30
47
  .new(Thredded.user_class.thredded_messageboards_readers([messageboard]), Thredded.user_name_column)
31
48
  .in(user_names)
32
49
  end
50
+
51
+ def auto_follow_and_notify
52
+ # need to do this in-process so that it appears to them immediately
53
+ UserTopicFollow.create_unless_exists(user.id, postable_id, :posted)
54
+ # everything else can happen later
55
+ AutoFollowAndNotifyJob.perform_later(id)
56
+ end
33
57
  end
34
58
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ module Thredded
3
+ class PostModerationRecord < ActiveRecord::Base
4
+ include ModerationState
5
+ # Rails 4 doesn't support enum _prefix
6
+ if Rails::VERSION::MAJOR >= 5
7
+ enum previous_moderation_state: moderation_states, _prefix: :previous
8
+ end
9
+ validates :previous_moderation_state, presence: true
10
+
11
+ belongs_to :messageboard, inverse_of: :post_moderation_records
12
+ validates :messageboard_id, presence: true
13
+ belongs_to :post, inverse_of: :moderation_records
14
+ belongs_to :post_user, class_name: Thredded.user_class, inverse_of: :thredded_post_moderation_records
15
+ belongs_to :moderator, class_name: Thredded.user_class, inverse_of: :thredded_post_moderation_records
16
+
17
+ validates_each :moderation_state do |record, attr, value|
18
+ if record.previous_moderation_state == value
19
+ record.errors.add attr, "Post moderation_state is already #{value}"
20
+ end
21
+ end
22
+
23
+ # @param [Thredded.user_class] moderator
24
+ # @param [Thredded::Post] post
25
+ # @param [Symbol, String] previous_moderation_state
26
+ # @param [Symbol, String] moderation_state
27
+ # @return [Thredded::PostModerationRecord] the newly created persisted record
28
+ def self.record!(moderator:, post:, previous_moderation_state:, moderation_state:)
29
+ # Rails 4 doesn't support enum _prefix
30
+ if Rails::VERSION::MAJOR < 5
31
+ previous_moderation_state = moderation_states[previous_moderation_state.to_s]
32
+ end
33
+ create!(
34
+ previous_moderation_state: previous_moderation_state,
35
+ moderation_state: moderation_state,
36
+ moderator: moderator,
37
+ post: post,
38
+ post_content: post.content,
39
+ post_user: post.user,
40
+ post_user_name: post.user.send(Thredded.user_name_column),
41
+ messageboard_id: post.messageboard_id,
42
+ )
43
+ end
44
+ end
45
+ end
@@ -15,15 +15,30 @@ module Thredded
15
15
  primary_key: :user_id,
16
16
  foreign_key: :user_id
17
17
 
18
+ after_commit :notify_users, on: [:create]
19
+
20
+ # @param [Integer] per_page
21
+ def page(per_page: self.class.default_per_page)
22
+ 1 + postable.posts.where(postable.posts.arel_table[:id].lt(id)).count / per_page
23
+ end
24
+
18
25
  def private_topic_post?
19
26
  true
20
27
  end
21
28
 
29
+ def user_detail
30
+ super || build_user_detail
31
+ end
32
+
22
33
  # @return [ActiveRecord::Relation<Thredded.user_class>] users from the list of user names that can read this post.
23
34
  def readers_from_user_names(user_names)
24
35
  DbTextSearch::CaseInsensitive
25
36
  .new(postable.users, Thredded.user_name_column)
26
37
  .in(user_names)
27
38
  end
39
+
40
+ def notify_users
41
+ Thredded::NotifyPrivateTopicUsersJob.perform_later(id)
42
+ end
28
43
  end
29
44
  end