thredded 0.2.2 → 0.3.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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.mkdn +63 -0
  3. data/Procfile +1 -0
  4. data/README.mkdn +42 -20
  5. data/app/assets/images/thredded/private-messages.svg +4 -0
  6. data/app/assets/images/thredded/settings.svg +4 -0
  7. data/app/assets/javascripts/thredded.es6 +2 -10
  8. data/app/assets/javascripts/thredded/{currently_online.es6 → components/currently_online.es6} +0 -0
  9. data/app/assets/javascripts/thredded/{post_form.es6 → components/post_form.es6} +0 -0
  10. data/app/assets/javascripts/thredded/{time_stamps.es6 → components/time_stamps.es6} +0 -0
  11. data/app/assets/javascripts/thredded/{topic_form.es6 → components/topic_form.es6} +1 -1
  12. data/app/assets/javascripts/thredded/components/topics.es6 +37 -0
  13. data/app/assets/javascripts/thredded/components/user_preferences_form.es6 +45 -0
  14. data/app/assets/javascripts/thredded/components/users_select.es6 +56 -0
  15. data/app/assets/javascripts/thredded/dependencies.js +9 -0
  16. data/app/assets/javascripts/thredded/thredded.es6 +1 -0
  17. data/app/assets/stylesheets/thredded/_base.scss +3 -2
  18. data/app/assets/stylesheets/thredded/_thredded.scss +4 -1
  19. data/app/assets/stylesheets/thredded/base/_buttons.scss +2 -1
  20. data/app/assets/stylesheets/thredded/base/_forms.scss +23 -18
  21. data/app/assets/stylesheets/thredded/base/_grid.scss +1 -1
  22. data/app/assets/stylesheets/thredded/base/_nav.scss +21 -0
  23. data/app/assets/stylesheets/thredded/base/_tables.scss +5 -14
  24. data/app/assets/stylesheets/thredded/base/_typography.scss +9 -4
  25. data/app/assets/stylesheets/thredded/base/_variables.scss +28 -9
  26. data/app/assets/stylesheets/thredded/components/_alerts.scss +19 -0
  27. data/app/assets/stylesheets/thredded/components/_currently-online.scss +1 -1
  28. data/app/assets/stylesheets/thredded/components/_form-list.scss +2 -4
  29. data/app/assets/stylesheets/thredded/components/_icons.scss +3 -0
  30. data/app/assets/stylesheets/thredded/components/_messageboard.scss +4 -4
  31. data/app/assets/stylesheets/thredded/components/_pagination.scss +2 -2
  32. data/app/assets/stylesheets/thredded/components/_post-form.scss +3 -0
  33. data/app/assets/stylesheets/thredded/components/_post.scss +14 -4
  34. data/app/assets/stylesheets/thredded/components/_select2.scss +79 -9
  35. data/app/assets/stylesheets/thredded/components/_topic-header.scss +11 -1
  36. data/app/assets/stylesheets/thredded/components/_topics.scss +13 -11
  37. data/app/assets/stylesheets/thredded/layout/_main-container.scss +3 -3
  38. data/app/assets/stylesheets/thredded/layout/_main-navigation.scss +11 -17
  39. data/app/assets/stylesheets/thredded/layout/_navigation.scss +72 -0
  40. data/app/assets/stylesheets/thredded/layout/_search-navigation.scss +66 -0
  41. data/app/assets/stylesheets/thredded/layout/_user-navigation.scss +35 -61
  42. data/app/commands/thredded/at_notification_extractor.rb +1 -0
  43. data/app/commands/thredded/members_marked_notified.rb +1 -0
  44. data/app/commands/thredded/messageboard_destroyer.rb +7 -2
  45. data/app/commands/thredded/notify_mentioned_users.rb +8 -21
  46. data/app/commands/thredded/notify_private_topic_users.rb +3 -5
  47. data/app/controllers/thredded/application_controller.rb +76 -41
  48. data/app/controllers/thredded/autocomplete_users_controller.rb +46 -0
  49. data/app/controllers/thredded/messageboards_controller.rb +8 -5
  50. data/app/controllers/thredded/posts_controller.rb +20 -22
  51. data/app/controllers/thredded/preferences_controller.rb +19 -14
  52. data/app/controllers/thredded/private_topics_controller.rb +58 -23
  53. data/app/controllers/thredded/setups_controller.rb +1 -0
  54. data/app/controllers/thredded/theme_previews_controller.rb +24 -53
  55. data/app/controllers/thredded/topics_controller.rb +48 -77
  56. data/app/forms/thredded/private_topic_form.rb +1 -21
  57. data/app/forms/thredded/topic_form.rb +3 -7
  58. data/app/forms/thredded/user_preferences_form.rb +62 -0
  59. data/app/helpers/thredded/application_helper.rb +11 -12
  60. data/app/helpers/thredded/urls_helper.rb +103 -0
  61. data/app/jobs/thredded/activity_updater_job.rb +4 -3
  62. data/app/jobs/thredded/at_notifier_job.rb +1 -0
  63. data/app/jobs/thredded/notify_private_topic_users_job.rb +1 -0
  64. data/app/mailer_previews/thredded/base_mailer_preview.rb +101 -0
  65. data/app/mailer_previews/thredded/post_mailer_preview.rb +11 -0
  66. data/app/mailer_previews/thredded/private_post_mailer_preview.rb +11 -0
  67. data/app/mailer_previews/thredded/private_topic_mailer_preview.rb +15 -0
  68. data/app/mailers/thredded/base_mailer.rb +13 -0
  69. data/app/mailers/thredded/post_mailer.rb +4 -2
  70. data/app/mailers/thredded/private_post_mailer.rb +4 -2
  71. data/app/mailers/thredded/private_topic_mailer.rb +4 -2
  72. data/app/models/concerns/thredded/friendly_id_reserved_words_and_pagination.rb +16 -0
  73. data/app/models/concerns/thredded/post_common.rb +68 -63
  74. data/app/models/concerns/thredded/topic_common.rb +31 -8
  75. data/app/models/concerns/thredded/user_topic_read_state_common.rb +31 -0
  76. data/app/models/thredded/category.rb +1 -0
  77. data/app/models/thredded/messageboard.rb +24 -25
  78. data/app/models/thredded/messageboard_user.rb +1 -0
  79. data/app/models/thredded/null_preference.rb +1 -0
  80. data/app/models/thredded/null_user.rb +1 -6
  81. data/app/models/thredded/null_user_topic_read_state.rb +12 -0
  82. data/app/models/thredded/post.rb +6 -9
  83. data/app/models/thredded/post_notification.rb +1 -0
  84. data/app/models/thredded/private_post.rb +6 -2
  85. data/app/models/thredded/private_topic.rb +46 -32
  86. data/app/models/thredded/private_user.rb +3 -2
  87. data/app/models/thredded/stats.rb +1 -0
  88. data/app/models/thredded/topic.rb +40 -64
  89. data/app/models/thredded/topic_category.rb +1 -0
  90. data/app/models/thredded/user_detail.rb +2 -15
  91. data/app/models/thredded/user_extender.rb +29 -14
  92. data/app/models/thredded/user_messageboard_preference.rb +20 -0
  93. data/app/models/thredded/user_permissions/admin/if_admin_column_true.rb +1 -0
  94. data/app/models/thredded/user_permissions/admin/none.rb +1 -0
  95. data/app/models/thredded/user_permissions/message/readers_of_writeable_boards.rb +1 -0
  96. data/app/models/thredded/user_permissions/moderate/if_moderator_column_true.rb +1 -0
  97. data/app/models/thredded/user_permissions/moderate/none.rb +1 -0
  98. data/app/models/thredded/user_permissions/read/all.rb +1 -0
  99. data/app/models/thredded/user_permissions/write/all.rb +1 -0
  100. data/app/models/thredded/user_permissions/write/none.rb +1 -0
  101. data/app/models/thredded/user_preference.rb +7 -1
  102. data/app/models/thredded/user_private_topic_read_state.rb +12 -0
  103. data/app/models/thredded/user_topic_read_state.rb +12 -0
  104. data/app/policies/thredded/messageboard_policy.rb +27 -0
  105. data/app/policies/thredded/post_policy.rb +33 -0
  106. data/app/policies/thredded/private_post_policy.rb +29 -0
  107. data/app/policies/thredded/private_topic_policy.rb +23 -0
  108. data/app/policies/thredded/topic_policy.rb +32 -0
  109. data/app/view_models/thredded/base_topic_view.rb +56 -0
  110. data/app/view_models/thredded/post_view.rb +44 -0
  111. data/app/view_models/thredded/posts_page_view.rb +27 -0
  112. data/app/view_models/thredded/private_topic_view.rb +9 -0
  113. data/app/{decorators/thredded/topic_email_decorator.rb → view_models/thredded/topic_email_view.rb} +2 -1
  114. data/app/view_models/thredded/topic_view.rb +23 -0
  115. data/app/view_models/thredded/topics_page_view.rb +26 -0
  116. data/app/views/thredded/error_pages/forbidden.html.erb +6 -0
  117. data/app/views/thredded/error_pages/not_found.html.erb +6 -0
  118. data/app/views/thredded/messageboards/_messageboard.html.erb +13 -6
  119. data/app/views/thredded/messageboards/index.html.erb +2 -8
  120. data/app/views/thredded/messageboards/new.html.erb +8 -2
  121. data/app/views/thredded/post_mailer/at_notification.html.erb +3 -3
  122. data/app/views/thredded/post_mailer/at_notification.text.erb +1 -1
  123. data/app/views/thredded/posts/_content_field.html.erb +1 -1
  124. data/app/views/thredded/posts/_form.html.erb +5 -1
  125. data/app/views/thredded/posts/_post.html.erb +3 -1
  126. data/app/views/thredded/posts/edit.html.erb +7 -3
  127. data/app/views/thredded/posts_common/_form.html.erb +1 -1
  128. data/app/views/thredded/posts_common/_post.html.erb +14 -8
  129. data/app/views/thredded/preferences/_form.html.erb +37 -15
  130. data/app/views/thredded/preferences/_header.html.erb +1 -1
  131. data/app/views/thredded/preferences/edit.html.erb +4 -6
  132. data/app/views/thredded/private_post_mailer/at_notification.html.erb +6 -4
  133. data/app/views/thredded/private_posts/_form.html.erb +5 -1
  134. data/app/views/thredded/private_posts/_private_post.html.erb +3 -1
  135. data/app/views/thredded/private_topic_mailer/message_notification.html.erb +3 -7
  136. data/app/views/thredded/private_topic_mailer/message_notification.text.erb +1 -3
  137. data/app/views/thredded/private_topics/_breadcrumbs.html.erb +2 -2
  138. data/app/views/thredded/private_topics/_form.html.erb +15 -10
  139. data/app/views/thredded/private_topics/_header.html.erb +12 -0
  140. data/app/views/thredded/private_topics/_no_private_topics.html.erb +2 -2
  141. data/app/views/thredded/private_topics/_private_topic.html.erb +4 -6
  142. data/app/views/thredded/private_topics/edit.html.erb +32 -0
  143. data/app/views/thredded/private_topics/index.html.erb +5 -5
  144. data/app/views/thredded/private_topics/new.html.erb +1 -2
  145. data/app/views/thredded/private_topics/show.html.erb +12 -7
  146. data/app/views/thredded/search/_form.html.erb +9 -6
  147. data/app/views/thredded/shared/{_messageboard_topics_breadcrumbs.html.erb → _breadcrumbs.html.erb} +2 -2
  148. data/app/views/thredded/shared/_header.html.erb +2 -3
  149. data/app/views/thredded/shared/_nav.html.erb +20 -0
  150. data/app/views/thredded/shared/nav/_notification_preferences.html.erb +6 -0
  151. data/app/views/thredded/shared/nav/_private_topics.html.erb +11 -0
  152. data/app/views/thredded/shared/nav/_standalone.html.erb +12 -0
  153. data/app/views/thredded/theme_previews/_section_title.html.erb +2 -2
  154. data/app/views/thredded/theme_previews/show.html.erb +13 -17
  155. data/app/views/thredded/topics/_form.html.erb +8 -6
  156. data/app/views/thredded/topics/_header.html.erb +12 -0
  157. data/app/views/thredded/topics/_topic.html.erb +4 -8
  158. data/app/views/thredded/topics/_topic_form_admin_options.html.erb +1 -1
  159. data/app/views/thredded/topics/edit.html.erb +22 -18
  160. data/app/views/thredded/topics/index.html.erb +3 -3
  161. data/app/views/thredded/topics/new.html.erb +1 -1
  162. data/app/views/thredded/topics/search.html.erb +17 -5
  163. data/app/views/thredded/topics/show.html.erb +14 -11
  164. data/bin/rails +5 -0
  165. data/config.ru +3 -0
  166. data/config/i18n-tasks.yml +16 -0
  167. data/config/locales/en.yml +90 -0
  168. data/config/routes.rb +29 -15
  169. data/db/migrate/20160329231848_create_thredded.rb +29 -33
  170. data/db/seeds.rb +115 -0
  171. data/db/upgrade_migrations/20160410111522_upgrade_v0_2_to_v0_3.rb +59 -0
  172. data/heroku.gemfile +26 -0
  173. data/heroku.gemfile.lock +282 -0
  174. data/lib/generators/thredded/install/install_generator.rb +1 -0
  175. data/lib/generators/thredded/install/templates/initializer.rb +17 -0
  176. data/lib/html/pipeline/at_mention_filter.rb +2 -1
  177. data/lib/html/pipeline/bbcode_filter.rb +13 -4
  178. data/lib/tasks/thredded_tasks.rake +1 -0
  179. data/lib/thredded.rb +19 -17
  180. data/lib/thredded/at_users.rb +1 -0
  181. data/lib/thredded/engine.rb +14 -5
  182. data/lib/thredded/errors.rb +11 -11
  183. data/lib/thredded/main_app_route_delegator.rb +1 -0
  184. data/lib/thredded/search_parser.rb +2 -1
  185. data/lib/thredded/topics_search.rb +67 -0
  186. data/lib/thredded/version.rb +2 -1
  187. data/thredded.gemspec +12 -8
  188. metadata +146 -82
  189. data/app/assets/javascripts/thredded/users_select.es6 +0 -5
  190. data/app/assets/stylesheets/thredded/layout/_topic-navigation.scss +0 -53
  191. data/app/commands/thredded/user_reads_private_topic.rb +0 -22
  192. data/app/commands/thredded/user_resets_private_topic_to_unread.rb +0 -23
  193. data/app/decorators/thredded/base_topic_decorator.rb +0 -14
  194. data/app/decorators/thredded/base_user_topic_decorator.rb +0 -63
  195. data/app/decorators/thredded/messageboard_decorator.rb +0 -41
  196. data/app/decorators/thredded/post_decorator.rb +0 -40
  197. data/app/decorators/thredded/private_topic_decorator.rb +0 -23
  198. data/app/decorators/thredded/topic_decorator.rb +0 -25
  199. data/app/decorators/thredded/user_private_topic_decorator.rb +0 -13
  200. data/app/decorators/thredded/user_topic_decorator.rb +0 -37
  201. data/app/models/thredded/ability.rb +0 -60
  202. data/app/models/thredded/notification_preference.rb +0 -17
  203. data/app/models/thredded/null_topic.rb +0 -15
  204. data/app/models/thredded/null_topic_read.rb +0 -19
  205. data/app/models/thredded/user_topic_read.rb +0 -10
  206. data/app/views/thredded/shared/_notification_preferences.html.erb +0 -7
  207. data/app/views/thredded/shared/_top_nav.html.erb +0 -36
  208. data/app/views/thredded/shared/_topic_nav.html.erb +0 -22
  209. data/app/views/thredded/topics/_recent_topics_by_user.html.erb +0 -8
  210. data/app/views/thredded/topics/by_category.html.erb +0 -56
  211. data/app/views/thredded/topics_common/_header.html.erb +0 -6
  212. data/lib/thredded/messageboard_user_permissions.rb +0 -22
  213. data/lib/thredded/post_sql_builder.rb +0 -12
  214. data/lib/thredded/post_user_permissions.rb +0 -32
  215. data/lib/thredded/private_topic_user_permissions.rb +0 -26
  216. data/lib/thredded/search_sql_builder.rb +0 -21
  217. data/lib/thredded/seed_database.rb +0 -76
  218. data/lib/thredded/table_sql_builder.rb +0 -41
  219. data/lib/thredded/topic_sql_builder.rb +0 -11
  220. data/lib/thredded/topic_user_permissions.rb +0 -32
@@ -1,105 +1,110 @@
1
+ # frozen_string_literal: true
1
2
  module Thredded
2
3
  module PostCommon
3
4
  extend ActiveSupport::Concern
5
+
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] + [
21
+ 'iframe',
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
+ },
31
+ add_attributes: {
32
+ 'iframe' => {
33
+ 'seamless' => 'seamless',
34
+ 'sandbox' => 'allow-same-origin allow-forms allow-scripts'
35
+ }
36
+ }
37
+ ).freeze
38
+
4
39
  included do
5
40
  paginates_per 50
6
41
 
7
- belongs_to :user, class_name: Thredded.user_class
8
42
  delegate :email, to: :user, prefix: true, allow_nil: true
9
43
 
10
44
  has_many :post_notifications, as: :post, dependent: :destroy
11
45
 
12
46
  validates :content, presence: true
13
47
 
14
- after_create :update_parent_last_user_and_timestamp
48
+ scope :order_oldest_first, -> { order(id: :asc) }
49
+
50
+ after_commit :update_parent_last_user_and_timestamp, on: [:create, :destroy]
15
51
  after_commit :notify_at_users, on: [:create, :update]
52
+ end
16
53
 
17
- extend ClassMethods
54
+ def page(per_page: self.class.default_per_page)
55
+ 1 + postable.posts.where('id < ?', id).count / per_page
18
56
  end
19
57
 
20
58
  def avatar_url
21
59
  Thredded.avatar_url.call(user)
22
60
  end
23
61
 
24
- def user_anonymous?
25
- user.try(:thredded_anonymous?)
26
- end
27
-
28
62
  # @param view_context [Object] the context of the rendering view.
29
63
  def filtered_content(view_context)
30
- pipeline = HTML::Pipeline.new(
31
- [
32
- html_filter_for_pipeline,
33
- HTML::Pipeline::SanitizationFilter,
34
- HTML::Pipeline::AtMentionFilter,
35
- HTML::Pipeline::EmojiFilter,
36
- HTML::Pipeline::AutolinkFilter,
37
- ], context_options)
38
-
64
+ pipeline = HTML::Pipeline.new(content_pipeline_filters, content_pipeline_options)
39
65
  result = pipeline.call(content, view_context: view_context)
40
66
  result[:output].to_s.html_safe
41
67
  end
42
68
 
43
- private
69
+ protected
70
+
71
+ # @return [Array<HTML::Pipeline::Filter]>]
72
+ def content_pipeline_filters
73
+ [
74
+ HTML::Pipeline::VimeoFilter,
75
+ HTML::Pipeline::YoutubeFilter,
76
+ HTML::Pipeline::BbcodeFilter,
77
+ HTML::Pipeline::MarkdownFilter,
78
+ HTML::Pipeline::SanitizationFilter,
79
+ HTML::Pipeline::AtMentionFilter,
80
+ HTML::Pipeline::EmojiFilter,
81
+ HTML::Pipeline::AutolinkFilter,
82
+ ]
83
+ end
44
84
 
45
- def context_options
85
+ # @return [Hash] options for HTML::Pipeline.new
86
+ def content_pipeline_options
46
87
  {
47
88
  asset_root: Rails.application.config.action_controller.asset_host || '',
48
89
  post: self,
49
- whitelist: sanitize_whitelist
90
+ whitelist: WHITELIST,
50
91
  }
51
92
  end
52
93
 
53
- def sanitize_whitelist
54
- HTML::Pipeline::SanitizationFilter::WHITELIST.deep_merge(
55
- attributes: {
56
- 'code' => ['class'],
57
- 'img' => %w(src class width height),
58
- 'blockquote' => ['class'],
59
- },
60
- transformers: [(lambda do |env|
61
- node = env[:node]
62
- node_name = env[:node_name]
63
-
64
- return if env[:is_whitelisted] || !node.element?
65
- return if node_name != 'iframe'
66
- return if (node['src'] =~ %r{\A(https?:)?//(?:www\.)?youtube(?:-nocookie)?\.com/}).nil?
67
-
68
- Sanitize.node!(
69
- node,
70
- elements: %w(iframe),
71
- attributes: {
72
- 'iframe' => %w(allowfullscreen frameborder height src width)
73
- }
74
- )
75
-
76
- { node_whitelist: [node] }
77
- end)]
78
- )
79
- end
80
-
81
- def html_filter_for_pipeline
82
- if filter == 'bbcode'
83
- HTML::Pipeline::BbcodeFilter
84
- else
85
- HTML::Pipeline::MarkdownFilter
86
- end
87
- end
94
+ private
88
95
 
89
96
  def update_parent_last_user_and_timestamp
90
- return unless postable && user
91
-
92
- postable.update!(last_user: user, updated_at: Time.zone.now)
97
+ return if postable.destroyed?
98
+ last_post = if destroyed?
99
+ postable.posts.order_oldest_first.select(:user_id, :created_at).last
100
+ else
101
+ self
102
+ end
103
+ postable.update!(last_user_id: last_post.user_id, updated_at: last_post.created_at)
93
104
  end
94
105
 
95
106
  def notify_at_users
96
107
  AtNotifierJob.perform_later(self.class.name, id)
97
108
  end
98
-
99
- module ClassMethods
100
- def filters
101
- %w(bbcode markdown)
102
- end
103
- end
104
109
  end
105
110
  end
@@ -1,16 +1,15 @@
1
+ # frozen_string_literal: true
1
2
  module Thredded
2
3
  module TopicCommon
3
4
  extend ActiveSupport::Concern
4
5
  included do
5
- paginates_per 50 if self.respond_to?(:paginates_per)
6
+ paginates_per 50 if respond_to?(:paginates_per)
6
7
 
7
- belongs_to :user,
8
- class_name: Thredded.user_class
9
8
  belongs_to :last_user,
10
- class_name: Thredded.user_class,
9
+ class_name: Thredded.user_class,
11
10
  foreign_key: 'last_user_id'
12
11
 
13
- scope :order_latest_first, -> { order(updated_at: :desc, id: :desc) }
12
+ scope :order_recently_updated_first, -> { order(updated_at: :desc, id: :desc) }
14
13
  scope :on_page, -> page_num { page(page_num).per(30) }
15
14
 
16
15
  validates_presence_of :hash_id
@@ -38,9 +37,33 @@ module Thredded
38
37
  end
39
38
 
40
39
  module ClassMethods
41
- def decorate
42
- all.map do |topic|
43
- TopicDecorator.new(topic)
40
+ # @param user [Thredded.user_class]
41
+ # @return [ActiveRecord::Relation]
42
+ def unread(user)
43
+ topics = arel_table
44
+ reads_class = reflect_on_association(:user_read_states).klass
45
+ reads = reads_class.arel_table
46
+ joins(topics.join(reads, Arel::Nodes::OuterJoin)
47
+ .on(topics[:id].eq(reads[:postable_id]).and(reads[:user_id].eq(user.id))).join_sources)
48
+ .merge(reads_class.where(reads[:id].eq(nil).or(reads[:read_at].lt(topics[:updated_at]))))
49
+ end
50
+
51
+ # @param user [Thredded.user_class]
52
+ # @return [Array<[TopicCommon, UserTopicReadStateCommon]>]
53
+ def with_read_states(user)
54
+ null_read_state = Thredded::NullUserTopicReadState.new
55
+ return current_scope.zip([null_read_state]) if user.thredded_anonymous?
56
+ read_state_by_topic_id =
57
+ reflect_on_association(:user_read_states).klass
58
+ .where(user_id: user.id, postable_id: current_scope.map(&:id))
59
+ .group_by(&:postable_id)
60
+ current_scope.map do |topic|
61
+ read_state = read_state_by_topic_id[topic.id]
62
+ if read_state
63
+ read_state = read_state[0]
64
+ read_state.postable = topic
65
+ end
66
+ [topic, read_state || null_read_state]
44
67
  end
45
68
  end
46
69
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module Thredded
3
+ module UserTopicReadStateCommon
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ extend ClassMethods
7
+ validates :user_id, uniqueness: { scope: :postable_id }
8
+ end
9
+
10
+ # @return [Boolean]
11
+ def read?
12
+ postable.updated_at <= read_at
13
+ end
14
+
15
+ module ClassMethods
16
+ # @param user_id [Fixnum]
17
+ # @param topic_id [Fixnum]
18
+ # @param post [Thredded::PostCommon]
19
+ # @param post_page [Fixnum]
20
+ def touch!(user_id, topic_id, post, post_page)
21
+ # TODO: Switch to upsert once Travis supports PostgreSQL 9.5.
22
+ # Travis issue: https://github.com/travis-ci/travis-ci/issues/4264
23
+ # Upsert gem: https://github.com/seamusabshere/upsert
24
+ state = where(user_id: user_id, postable_id: topic_id).first_or_initialize
25
+ fail ArgumentError, "expected post_page >= 1, given #{post_page.inspect}" if post_page < 1
26
+ return unless !state.read_at? || state.read_at < post.updated_at
27
+ state.update!(read_at: post.updated_at, page: post_page)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Thredded
2
3
  class Category < ActiveRecord::Base
3
4
  extend FriendlyId
@@ -1,50 +1,49 @@
1
+ # frozen_string_literal: true
1
2
  module Thredded
2
3
  class Messageboard < ActiveRecord::Base
3
- FILTERS = %w(markdown bbcode)
4
-
5
4
  extend FriendlyId
6
- friendly_id :name, use: :slugged
5
+ friendly_id :slug_candidates,
6
+ use: [:slugged, :reserved],
7
+ # Avoid route conflicts
8
+ reserved_words: ::Thredded::FriendlyIdReservedWordsAndPagination.new(
9
+ %w(messageboards preferences private-topics autocomplete-users theme-preview)
10
+ )
7
11
 
8
- validates :filter, inclusion: { in: FILTERS }, presence: true
9
12
  validates :name, uniqueness: true, length: { maximum: 60 }, presence: true
10
- validates :slug, exclusion: { in: %w(private-topics) }
11
13
  validates :topics_count, numericality: true
12
14
 
13
15
  has_many :categories, dependent: :destroy
14
- has_many :notification_preferences, dependent: :destroy
16
+ has_many :user_messageboard_preferences, dependent: :destroy
15
17
  has_many :posts, dependent: :destroy
16
- has_many :topics, dependent: :destroy
17
- has_many :user_details, through: :posts
18
+ has_many :topics, dependent: :destroy, inverse_of: :messageboard
19
+ has_one :latest_topic, -> { order_recently_updated_first },
20
+ class_name: 'Thredded::Topic'
18
21
 
22
+ has_many :user_details, through: :posts
19
23
  has_many :messageboard_users,
20
- class_name: 'Thredded::MessageboardUser',
21
- inverse_of: :messageboard,
24
+ inverse_of: :messageboard,
22
25
  foreign_key: :thredded_messageboard_id
23
26
  has_many :recently_active_user_details,
24
27
  -> { merge(Thredded::MessageboardUser.recently_active) },
25
28
  class_name: 'Thredded::UserDetail',
26
- through: :messageboard_users,
27
- source: :user_detail
29
+ through: :messageboard_users,
30
+ source: :user_detail
28
31
  has_many :recently_active_users,
29
32
  class_name: Thredded.user_class,
30
- through: :recently_active_user_details,
31
- source: :user
33
+ through: :recently_active_user_details,
34
+ source: :user
32
35
 
33
36
  default_scope { where(closed: false).order(topics_count: :desc) }
34
37
 
35
- def self.decorate
36
- all.map do |messageboard|
37
- MessageboardDecorator.new(messageboard)
38
- end
39
- end
40
-
41
- def preferences_for(user)
42
- @preferences_for ||=
43
- notification_preferences.where(user_id: user).first_or_create
38
+ def latest_user
39
+ latest_topic.last_user
44
40
  end
45
41
 
46
- def decorate
47
- MessageboardDecorator.new(self)
42
+ def slug_candidates
43
+ [
44
+ :name,
45
+ [:name, '-board']
46
+ ]
48
47
  end
49
48
  end
50
49
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Thredded
2
3
  # The state of a user with regards to a messageboard, such as the last time the user was active (visited)
3
4
  # the messageboard.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Thredded
2
3
  class NullPreference
3
4
  def notify_on_mention
@@ -1,9 +1,4 @@
1
- require_relative './user_permissions/read/all'
2
- require_relative './user_permissions/write/none'
3
- require_relative './user_permissions/message/readers_of_writeable_boards'
4
- require_relative './user_permissions/moderate/none'
5
- require_relative './user_permissions/admin/none'
6
-
1
+ # frozen_string_literal: true
7
2
  module Thredded
8
3
  class NullUser
9
4
  include ::Thredded::UserPermissions::Read::All
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module Thredded
3
+ class NullUserTopicReadState
4
+ def page
5
+ 1
6
+ end
7
+
8
+ def read?
9
+ false
10
+ end
11
+ end
12
+ end
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  module Thredded
2
3
  class Post < ActiveRecord::Base
3
4
  include PostCommon
4
5
 
6
+ belongs_to :user,
7
+ class_name: Thredded.user_class,
8
+ inverse_of: :thredded_posts
5
9
  belongs_to :messageboard,
6
10
  counter_cache: true
7
11
  belongs_to :postable,
@@ -15,7 +19,6 @@ module Thredded
15
19
  counter_cache: true
16
20
 
17
21
  validates :messageboard_id, presence: true
18
- before_validation :set_filter_from_messageboard
19
22
 
20
23
  def private_topic_post?
21
24
  false
@@ -23,15 +26,9 @@ module Thredded
23
26
 
24
27
  # @return [ActiveRecord::Relation<Thredded.user_class>] users from the list of user names that can read this post.
25
28
  def readers_from_user_names(user_names)
26
- DbTextSearch::CaseInsensitiveEq
29
+ DbTextSearch::CaseInsensitive
27
30
  .new(Thredded.user_class.thredded_messageboards_readers([messageboard]), Thredded.user_name_column)
28
- .find(user_names)
29
- end
30
-
31
- private
32
-
33
- def set_filter_from_messageboard
34
- self.filter = messageboard.filter if messageboard
31
+ .in(user_names)
35
32
  end
36
33
  end
37
34
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Thredded
2
3
  # Keeps track of post notifications that have been sent already.
3
4
  class PostNotification < ActiveRecord::Base
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  module Thredded
2
3
  class PrivatePost < ActiveRecord::Base
3
4
  include PostCommon
4
5
 
6
+ belongs_to :user,
7
+ class_name: Thredded.user_class,
8
+ inverse_of: :thredded_private_posts
5
9
  belongs_to :postable,
6
10
  class_name: 'Thredded::PrivateTopic',
7
11
  inverse_of: :posts,
@@ -17,9 +21,9 @@ module Thredded
17
21
 
18
22
  # @return [ActiveRecord::Relation<Thredded.user_class>] users from the list of user names that can read this post.
19
23
  def readers_from_user_names(user_names)
20
- DbTextSearch::CaseInsensitiveEq
24
+ DbTextSearch::CaseInsensitive
21
25
  .new(postable.users, Thredded.user_name_column)
22
- .find(user_names)
26
+ .in(user_names)
23
27
  end
24
28
  end
25
29
  end