thredded 0.1.0 → 0.2.2

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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.mkdn +202 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.mkdn +391 -0
  5. data/app/assets/images/thredded/breadcrumb-chevron.svg +1 -0
  6. data/app/assets/javascripts/thredded/currently_online.es6 +25 -0
  7. data/app/assets/javascripts/thredded/post_form.es6 +20 -0
  8. data/app/assets/javascripts/thredded/time_stamps.es6 +8 -0
  9. data/app/assets/javascripts/thredded/topic_form.es6 +55 -0
  10. data/app/assets/javascripts/thredded/users_select.es6 +5 -0
  11. data/app/assets/javascripts/thredded.es6 +10 -0
  12. data/app/assets/stylesheets/thredded/_base.scss +11 -0
  13. data/app/assets/stylesheets/thredded/_dependencies.scss +1 -0
  14. data/app/assets/stylesheets/thredded/_thredded.scss +27 -0
  15. data/app/assets/stylesheets/thredded/base/_alerts.scss +40 -0
  16. data/app/assets/stylesheets/thredded/base/_buttons.scss +39 -0
  17. data/app/assets/stylesheets/thredded/base/_forms.scss +79 -0
  18. data/app/assets/stylesheets/thredded/base/_grid.scss +19 -0
  19. data/app/assets/stylesheets/thredded/base/_lists.scss +31 -0
  20. data/app/assets/stylesheets/thredded/base/_tables.scss +25 -0
  21. data/app/assets/stylesheets/thredded/base/_typography.scss +36 -0
  22. data/app/assets/stylesheets/thredded/base/_variables.scss +54 -0
  23. data/app/assets/stylesheets/thredded/components/_base.scss +13 -0
  24. data/app/assets/stylesheets/thredded/components/_currently-online.scss +48 -0
  25. data/app/assets/stylesheets/thredded/components/_empty.scss +11 -0
  26. data/app/assets/stylesheets/thredded/components/_flash-message.scss +19 -0
  27. data/app/assets/stylesheets/thredded/components/_form-list.scss +35 -0
  28. data/app/assets/stylesheets/thredded/components/_main-section.scss +3 -0
  29. data/app/assets/stylesheets/thredded/components/_messageboard.scss +55 -0
  30. data/app/assets/stylesheets/thredded/components/_pagination.scss +26 -0
  31. data/app/assets/stylesheets/thredded/components/_post-form.scss +21 -0
  32. data/app/assets/stylesheets/thredded/components/_post.scss +66 -0
  33. data/app/assets/stylesheets/thredded/components/_preferences.scss +6 -0
  34. data/app/assets/stylesheets/thredded/components/_select2.scss +42 -0
  35. data/app/assets/stylesheets/thredded/components/_topic-delete.scss +9 -0
  36. data/app/assets/stylesheets/thredded/components/_topic-header.scss +20 -0
  37. data/app/assets/stylesheets/thredded/components/_topics.scss +113 -0
  38. data/app/assets/stylesheets/thredded/layout/_main-container.scss +15 -0
  39. data/app/assets/stylesheets/thredded/layout/_main-navigation.scss +45 -0
  40. data/app/assets/stylesheets/thredded/layout/_topic-navigation.scss +53 -0
  41. data/app/assets/stylesheets/thredded/layout/_user-navigation.scss +87 -0
  42. data/app/assets/stylesheets/thredded/utilities/_is-compact.scss +26 -0
  43. data/app/assets/stylesheets/thredded/utilities/_is-expanded.scss +16 -0
  44. data/app/assets/stylesheets/thredded.scss +3 -0
  45. data/app/commands/thredded/at_notification_extractor.rb +15 -0
  46. data/app/commands/thredded/members_marked_notified.rb +18 -0
  47. data/app/commands/thredded/messageboard_destroyer.rb +60 -0
  48. data/app/commands/thredded/notify_mentioned_users.rb +68 -0
  49. data/app/commands/thredded/notify_private_topic_users.rb +51 -0
  50. data/app/commands/thredded/user_reads_private_topic.rb +22 -0
  51. data/app/commands/thredded/user_resets_private_topic_to_unread.rb +23 -0
  52. data/app/controllers/thredded/application_controller.rb +105 -0
  53. data/app/controllers/thredded/messageboards_controller.rb +58 -0
  54. data/app/controllers/thredded/posts_controller.rb +78 -0
  55. data/app/controllers/thredded/preferences_controller.rb +28 -0
  56. data/app/controllers/thredded/private_topics_controller.rb +65 -0
  57. data/app/controllers/thredded/setups_controller.rb +53 -0
  58. data/app/controllers/thredded/theme_previews_controller.rb +59 -0
  59. data/app/controllers/thredded/topics_controller.rb +153 -0
  60. data/app/decorators/thredded/base_topic_decorator.rb +14 -0
  61. data/app/decorators/thredded/base_user_topic_decorator.rb +63 -0
  62. data/app/decorators/thredded/messageboard_decorator.rb +41 -0
  63. data/app/decorators/thredded/post_decorator.rb +40 -0
  64. data/app/decorators/thredded/private_topic_decorator.rb +23 -0
  65. data/app/decorators/thredded/topic_decorator.rb +25 -0
  66. data/app/decorators/thredded/topic_email_decorator.rb +24 -0
  67. data/app/decorators/thredded/user_private_topic_decorator.rb +13 -0
  68. data/app/decorators/thredded/user_topic_decorator.rb +37 -0
  69. data/app/forms/thredded/private_topic_form.rb +126 -0
  70. data/app/forms/thredded/topic_form.rb +94 -0
  71. data/app/helpers/thredded/application_helper.rb +41 -0
  72. data/app/jobs/thredded/activity_updater_job.rb +20 -0
  73. data/app/jobs/thredded/at_notifier_job.rb +11 -0
  74. data/app/jobs/thredded/notify_private_topic_users_job.rb +11 -0
  75. data/app/mailers/thredded/base_mailer.rb +4 -0
  76. data/app/mailers/thredded/post_mailer.rb +15 -0
  77. data/app/mailers/thredded/private_post_mailer.rb +15 -0
  78. data/app/mailers/thredded/private_topic_mailer.rb +15 -0
  79. data/app/models/concerns/thredded/post_common.rb +105 -0
  80. data/app/models/concerns/thredded/topic_common.rb +48 -0
  81. data/app/models/thredded/ability.rb +60 -0
  82. data/app/models/thredded/category.rb +12 -0
  83. data/app/models/thredded/messageboard.rb +50 -0
  84. data/app/models/thredded/messageboard_user.rb +12 -0
  85. data/app/models/thredded/notification_preference.rb +17 -0
  86. data/app/models/thredded/null_preference.rb +11 -0
  87. data/app/models/thredded/null_topic.rb +15 -0
  88. data/app/models/thredded/null_topic_read.rb +19 -0
  89. data/app/models/thredded/null_user.rb +51 -0
  90. data/app/models/thredded/post.rb +37 -0
  91. data/app/models/thredded/post_notification.rb +17 -0
  92. data/app/models/thredded/private_post.rb +25 -0
  93. data/app/models/thredded/private_topic.rb +60 -0
  94. data/app/models/thredded/private_user.rb +6 -0
  95. data/app/models/thredded/stats.rb +37 -0
  96. data/app/models/thredded/topic.rb +104 -0
  97. data/app/models/thredded/topic_category.rb +6 -0
  98. data/app/models/thredded/user_detail.rb +26 -0
  99. data/app/models/thredded/user_extender.rb +33 -0
  100. data/app/models/thredded/user_permissions/admin/if_admin_column_true.rb +12 -0
  101. data/app/models/thredded/user_permissions/admin/none.rb +12 -0
  102. data/app/models/thredded/user_permissions/message/readers_of_writeable_boards.rb +13 -0
  103. data/app/models/thredded/user_permissions/moderate/if_moderator_column_true.rb +25 -0
  104. data/app/models/thredded/user_permissions/moderate/none.rb +25 -0
  105. data/app/models/thredded/user_permissions/read/all.rb +25 -0
  106. data/app/models/thredded/user_permissions/write/all.rb +25 -0
  107. data/app/models/thredded/user_permissions/write/none.rb +25 -0
  108. data/app/models/thredded/user_preference.rb +6 -0
  109. data/app/models/thredded/user_topic_read.rb +10 -0
  110. data/app/views/layouts/thredded/application.html.erb +15 -0
  111. data/app/views/thredded/categories/_category.html.erb +1 -0
  112. data/app/views/thredded/kaminari/_first_page.html.erb +11 -0
  113. data/app/views/thredded/kaminari/_gap.html.erb +8 -0
  114. data/app/views/thredded/kaminari/_last_page.html.erb +11 -0
  115. data/app/views/thredded/kaminari/_next_page.html.erb +11 -0
  116. data/app/views/thredded/kaminari/_page.html.erb +12 -0
  117. data/app/views/thredded/kaminari/_paginator.html.erb +23 -0
  118. data/app/views/thredded/kaminari/_prev_page.html.erb +11 -0
  119. data/app/views/thredded/messageboards/_messageboard.html.erb +15 -0
  120. data/app/views/thredded/messageboards/index.html.erb +20 -0
  121. data/app/views/thredded/messageboards/new.html.erb +18 -0
  122. data/app/views/thredded/post_mailer/at_notification.html.erb +14 -0
  123. data/app/views/thredded/post_mailer/at_notification.text.erb +10 -0
  124. data/app/views/thredded/posts/_content_field.html.erb +4 -0
  125. data/app/views/thredded/posts/_form.html.erb +1 -0
  126. data/app/views/thredded/posts/_post.html.erb +1 -0
  127. data/app/views/thredded/posts/_user.html.erb +3 -0
  128. data/app/views/thredded/posts/edit.html.erb +13 -0
  129. data/app/views/thredded/posts_common/_form.html.erb +10 -0
  130. data/app/views/thredded/posts_common/_post.html.erb +20 -0
  131. data/app/views/thredded/preferences/_form.html.erb +28 -0
  132. data/app/views/thredded/preferences/_header.html.erb +1 -0
  133. data/app/views/thredded/preferences/edit.html.erb +12 -0
  134. data/app/views/thredded/private_post_mailer/at_notification.html.erb +11 -0
  135. data/app/views/thredded/private_posts/_form.html.erb +1 -0
  136. data/app/views/thredded/private_posts/_private_post.html.erb +1 -0
  137. data/app/views/thredded/private_topic_mailer/message_notification.html.erb +17 -0
  138. data/app/views/thredded/private_topic_mailer/message_notification.text.erb +13 -0
  139. data/app/views/thredded/private_topics/_breadcrumbs.html.erb +4 -0
  140. data/app/views/thredded/private_topics/_form.html.erb +24 -0
  141. data/app/views/thredded/private_topics/_no_private_topics.html.erb +6 -0
  142. data/app/views/thredded/private_topics/_private_topic.html.erb +22 -0
  143. data/app/views/thredded/private_topics/index.html.erb +23 -0
  144. data/app/views/thredded/private_topics/new.html.erb +13 -0
  145. data/app/views/thredded/private_topics/show.html.erb +14 -0
  146. data/app/views/thredded/search/_form.html.erb +7 -0
  147. data/app/views/thredded/shared/_currently_online.html.erb +16 -0
  148. data/app/views/thredded/shared/_flash_messages.html.erb +7 -0
  149. data/app/views/thredded/shared/_header.html.erb +4 -0
  150. data/app/views/thredded/shared/_messageboard_topics_breadcrumbs.html.erb +6 -0
  151. data/app/views/thredded/shared/_notification_preferences.html.erb +7 -0
  152. data/app/views/thredded/shared/_page.html.erb +6 -0
  153. data/app/views/thredded/shared/_time_ago.html.erb +7 -0
  154. data/app/views/thredded/shared/_top_nav.html.erb +36 -0
  155. data/app/views/thredded/shared/_topic_nav.html.erb +22 -0
  156. data/app/views/thredded/theme_previews/_section_title.html.erb +3 -0
  157. data/app/views/thredded/theme_previews/show.html.erb +108 -0
  158. data/app/views/thredded/topics/_form.html.erb +23 -0
  159. data/app/views/thredded/topics/_recent_topics_by_user.html.erb +8 -0
  160. data/app/views/thredded/topics/_topic.html.erb +29 -0
  161. data/app/views/thredded/topics/_topic_form_admin_options.html.erb +12 -0
  162. data/app/views/thredded/topics/by_category.html.erb +56 -0
  163. data/app/views/thredded/topics/edit.html.erb +38 -0
  164. data/app/views/thredded/topics/index.html.erb +18 -0
  165. data/app/views/thredded/topics/menu/_new_topic.html.erb +3 -0
  166. data/app/views/thredded/topics/new.html.erb +11 -0
  167. data/app/views/thredded/topics/search.html.erb +9 -0
  168. data/app/views/thredded/topics/show.html.erb +31 -0
  169. data/app/views/thredded/topics_common/_header.html.erb +6 -0
  170. data/app/views/thredded/users/_link.html.erb +9 -0
  171. data/config/routes.rb +28 -0
  172. data/db/migrate/20160329231848_create_thredded.rb +173 -0
  173. data/lib/generators/thredded/install/USAGE +13 -0
  174. data/lib/generators/thredded/install/install_generator.rb +23 -0
  175. data/lib/generators/thredded/install/templates/initializer.rb +51 -0
  176. data/lib/html/pipeline/at_mention_filter.rb +20 -0
  177. data/lib/html/pipeline/bbcode_filter.rb +19 -0
  178. data/lib/tasks/thredded_tasks.rake +18 -0
  179. data/lib/thredded/at_users.rb +19 -0
  180. data/lib/thredded/engine.rb +35 -0
  181. data/lib/thredded/errors.rb +67 -0
  182. data/lib/thredded/main_app_route_delegator.rb +24 -0
  183. data/lib/thredded/messageboard_user_permissions.rb +22 -0
  184. data/lib/thredded/post_sql_builder.rb +12 -0
  185. data/lib/thredded/post_user_permissions.rb +32 -0
  186. data/lib/thredded/private_topic_user_permissions.rb +26 -0
  187. data/lib/thredded/search_parser.rb +45 -0
  188. data/lib/thredded/search_sql_builder.rb +21 -0
  189. data/lib/thredded/seed_database.rb +76 -0
  190. data/lib/thredded/table_sql_builder.rb +41 -0
  191. data/lib/thredded/topic_sql_builder.rb +11 -0
  192. data/lib/thredded/topic_user_permissions.rb +32 -0
  193. data/lib/thredded/version.rb +3 -0
  194. data/lib/thredded.rb +85 -0
  195. data/thredded.gemspec +68 -0
  196. metadata +248 -122
@@ -0,0 +1,14 @@
1
+ module Thredded
2
+ class BaseTopicDecorator < SimpleDelegator
3
+ include Rails.application.routes.url_helpers
4
+ include ActionView::Helpers::UrlHelper
5
+
6
+ def slug
7
+ __getobj__.slug.nil? ? id : __getobj__.slug
8
+ end
9
+
10
+ def original
11
+ __getobj__
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,63 @@
1
+ module Thredded
2
+ class BaseUserTopicDecorator < SimpleDelegator
3
+ extend ActiveModel::Naming
4
+ include ActiveModel::Conversion
5
+
6
+ class << self
7
+ # @return [Class<ActiveRecord::Base>]
8
+ def topic_class
9
+ fail 'Implement in subclass'
10
+ end
11
+
12
+ def decorator_class
13
+ "#{topic_class.name}Decorator".constantize
14
+ end
15
+
16
+ def decorate_all(user, topics)
17
+ topics.map { |topic| new(user, topic) }
18
+ end
19
+
20
+ def model_name
21
+ ActiveModel::Name.new(self, nil, topic_class.name.demodulize)
22
+ end
23
+ end
24
+
25
+ def initialize(user, topic)
26
+ @user = user || Thredded::NullUser.new
27
+ @topic = self.class.decorator_class.new(topic)
28
+ super(@topic)
29
+ end
30
+
31
+ def to_model
32
+ topic
33
+ end
34
+
35
+ def persisted?
36
+ false
37
+ end
38
+
39
+ def css_class
40
+ [read_status_class, topic.css_class].map(&:presence).compact.join(' ')
41
+ end
42
+
43
+ def read_status_class
44
+ if read?
45
+ 'thredded--topic--read'
46
+ else
47
+ 'thredded--topic--unread'
48
+ end
49
+ end
50
+
51
+ def read?
52
+ fail 'Subclass responsibility'
53
+ end
54
+
55
+ def to_ary
56
+ [self]
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :topic, :user
62
+ end
63
+ end
@@ -0,0 +1,41 @@
1
+ module Thredded
2
+ class MessageboardDecorator < SimpleDelegator
3
+ include ActionView::Helpers::NumberHelper
4
+ include ActionView::Helpers::TagHelper
5
+
6
+ def initialize(messageboard)
7
+ super
8
+ @messageboard = messageboard
9
+ end
10
+
11
+ def original
12
+ messageboard
13
+ end
14
+
15
+ def meta
16
+ topics_count = number_to_human(messageboard.topics_count)
17
+ posts_count = number_to_human(messageboard.posts_count)
18
+
19
+ "#{topics_count} topics / #{posts_count} posts".downcase
20
+ end
21
+
22
+ def latest_topic
23
+ @latest_topic ||= begin
24
+ messageboard.topics.order_latest_first.first ||
25
+ Thredded::NullTopic.new
26
+ end
27
+ end
28
+
29
+ def latest_user
30
+ latest_topic.last_user
31
+ end
32
+
33
+ def category_options
34
+ messageboard.categories.map { |cat| [cat.name, cat.id] }
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :messageboard
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ module Thredded
2
+ class PostDecorator < SimpleDelegator
3
+ attr_reader :post
4
+
5
+ def initialize(post)
6
+ super
7
+ @post = post
8
+ end
9
+
10
+ def user_name
11
+ if user
12
+ user.to_s
13
+ else
14
+ 'Anonymous'
15
+ end
16
+ end
17
+
18
+ def original
19
+ post
20
+ end
21
+
22
+ def avatar_url
23
+ super.sub(/\Ahttp:/, '')
24
+ end
25
+
26
+ def to_ary
27
+ [self]
28
+ end
29
+
30
+ private
31
+
32
+ def created_at_str
33
+ created_at.getutc.to_s
34
+ end
35
+
36
+ def created_at_utc
37
+ created_at.getutc.iso8601
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ require 'thredded/base_topic_decorator'
2
+
3
+ module Thredded
4
+ class PrivateTopicDecorator < SimpleDelegator
5
+ def initialize(private_topic)
6
+ super Thredded::BaseTopicDecorator.new(private_topic)
7
+ end
8
+
9
+ def self.model_name
10
+ ActiveModel::Name.new(self, nil, 'PrivateTopic')
11
+ end
12
+
13
+ def to_model
14
+ __getobj__
15
+ end
16
+
17
+ def css_class
18
+ classes = []
19
+ classes << 'thredded--private-topic'
20
+ classes.join(' ')
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ require 'thredded/base_topic_decorator'
2
+
3
+ module Thredded
4
+ class TopicDecorator < SimpleDelegator
5
+ def initialize(private_topic)
6
+ super(Thredded::BaseTopicDecorator.new(private_topic))
7
+ end
8
+
9
+ def self.model_name
10
+ ActiveModel::Name.new(self, nil, 'Topic')
11
+ end
12
+
13
+ def css_class
14
+ classes = []
15
+ classes << 'thredded--topic--locked' if locked?
16
+ classes << 'thredded--topic--sticky' if sticky?
17
+ classes += ['thredded--topic--category'] + categories.map { |c| "thredded--topic--category--#{c.name}" } if categories.present?
18
+ classes.join(' ')
19
+ end
20
+
21
+ def category_options
22
+ messageboard.decorate.category_options
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module Thredded
2
+ class TopicEmailDecorator
3
+ # @param [Thredded::TopicCommon] topic
4
+ def initialize(topic)
5
+ @topic = topic
6
+ end
7
+
8
+ def smtp_api_tag(tag)
9
+ %({"category": ["thredded_#{@topic.private? ? 'private_topic' : @topic.messageboard.name}","#{tag}"]})
10
+ end
11
+
12
+ def subject
13
+ "#{Thredded.email_outgoing_prefix} #{@topic.title}"
14
+ end
15
+
16
+ def reply_to
17
+ Thredded.email_reply_to.call(@topic)
18
+ end
19
+
20
+ def no_reply
21
+ Thredded.email_from
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ require 'thredded/base_user_topic_decorator'
2
+
3
+ module Thredded
4
+ class UserPrivateTopicDecorator < BaseUserTopicDecorator
5
+ def self.topic_class
6
+ PrivateTopic
7
+ end
8
+
9
+ def read?
10
+ topic.private_users.find_by(user: user).try(:read?)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ require 'thredded/base_user_topic_decorator'
2
+
3
+ module Thredded
4
+ class UserTopicDecorator < BaseUserTopicDecorator
5
+ def self.topic_class
6
+ Topic
7
+ end
8
+
9
+ def farthest_page
10
+ read_status.page
11
+ end
12
+
13
+ def farthest_post
14
+ read_status.farthest_post
15
+ end
16
+
17
+ def read?
18
+ topic.posts_count == read_status.posts_count
19
+ end
20
+
21
+ private
22
+
23
+ def read_status
24
+ if user.id > 0
25
+ @read_status ||= topic.user_topic_reads.select do |reads|
26
+ reads.user_id == user.id
27
+ end
28
+ end
29
+
30
+ if @read_status.blank?
31
+ Thredded::NullTopicRead.new
32
+ else
33
+ @read_status.first
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,126 @@
1
+ module Thredded
2
+ class PrivateTopicForm
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor \
6
+ :title,
7
+ :category_ids,
8
+ :user_ids,
9
+ :locked,
10
+ :sticky,
11
+ :content,
12
+ :private_topic
13
+
14
+ attr_reader :user, :params
15
+
16
+ validate :validate_children
17
+
18
+ def initialize(params = {})
19
+ @params = params
20
+ @title = params[:title]
21
+ @category_ids = params[:category_ids] || []
22
+ @user_ids = params[:user_ids] || []
23
+ @user = params[:user] || fail('user is required')
24
+ @locked = params[:locked]
25
+ @sticky = params[:sticky]
26
+ @content = params[:content]
27
+ end
28
+
29
+ def self.model_name
30
+ Thredded::PrivateTopic.model_name
31
+ end
32
+
33
+ def users
34
+ @user.thredded_can_message_users
35
+ end
36
+
37
+ def users_for_select
38
+ (users - [@user]).map { |user| [user.to_s, user.id] }
39
+ end
40
+
41
+ def id
42
+ private_topic.id
43
+ end
44
+
45
+ def save
46
+ return false unless valid?
47
+
48
+ ActiveRecord::Base.transaction do
49
+ private_topic.save!
50
+ post.save!
51
+ end
52
+ true
53
+ end
54
+
55
+ def private_topic
56
+ @private_topic ||= Thredded::PrivateTopic.new(
57
+ title: title,
58
+ users: private_users,
59
+ user: non_null_user,
60
+ last_user: non_null_user)
61
+ end
62
+
63
+ def post
64
+ @post ||= private_topic.posts.build(
65
+ content: content,
66
+ user: non_null_user)
67
+ end
68
+
69
+ def users_selected_options
70
+ { selected: private_user_ids }
71
+ end
72
+
73
+ def users_select_html_options
74
+ {
75
+ multiple: true,
76
+ required: true,
77
+ 'data-placeholder' => 'select users to participate in this topic',
78
+ 'data-thredded-users-select' => true
79
+ }
80
+ end
81
+
82
+ private
83
+
84
+ def topic_categories
85
+ if category_ids
86
+ ids = category_ids.reject(&:empty?).map(&:to_i)
87
+ Category.where(id: ids)
88
+ else
89
+ []
90
+ end
91
+ end
92
+
93
+ def private_users
94
+ Thredded.user_class.where(id: normalized_user_ids)
95
+ end
96
+
97
+ def private_user_ids
98
+ private_users.map(&:id)
99
+ end
100
+
101
+ def normalized_user_ids
102
+ user_ids
103
+ .reject(&:empty?)
104
+ .map(&:to_i)
105
+ .push(user.id)
106
+ .uniq
107
+ end
108
+
109
+ # @return [Thredded.user_class, nil] return a user or nil if the user is a NullUser
110
+ # This is necessary because assigning a NullUser to an ActiveRecord association results in an exception.
111
+ def non_null_user
112
+ @user unless @user.thredded_anonymous?
113
+ end
114
+
115
+ def validate_children
116
+ promote_errors(private_topic.errors) if private_topic.invalid?
117
+ promote_errors(post.errors) if post.invalid?
118
+ end
119
+
120
+ def promote_errors(child_errors)
121
+ child_errors.each do |attribute, message|
122
+ errors.add(attribute, message)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,94 @@
1
+ module Thredded
2
+ class TopicForm
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :title, :category_ids, :locked, :sticky, :content, :topic
6
+ attr_reader :user, :messageboard
7
+
8
+ validate :validate_children
9
+
10
+ def initialize(params = {})
11
+ @title = params[:title]
12
+ @category_ids = params[:category_ids]
13
+ @locked = params[:locked] || false
14
+ @sticky = params[:sticky] || false
15
+ @content = params[:content]
16
+ @user = params[:user] || fail('user is required')
17
+ @messageboard = params[:messageboard]
18
+ end
19
+
20
+ def self.model_name
21
+ Thredded::Topic.model_name
22
+ end
23
+
24
+ def categories
25
+ topic.messageboard.categories
26
+ end
27
+
28
+ def category_options
29
+ topic.messageboard.decorate.category_options
30
+ end
31
+
32
+ def filter
33
+ topic.messageboard.filter
34
+ end
35
+
36
+ def save
37
+ return false unless valid?
38
+
39
+ ActiveRecord::Base.transaction do
40
+ topic.save!
41
+ post.save!
42
+ end
43
+ true
44
+ end
45
+
46
+ def topic
47
+ @topic ||= messageboard.topics.build(
48
+ title: title,
49
+ locked: locked,
50
+ sticky: sticky,
51
+ user: non_null_user,
52
+ last_user: non_null_user,
53
+ categories: topic_categories,
54
+ )
55
+ end
56
+
57
+ def post
58
+ @post ||= topic.posts.build(
59
+ content: content,
60
+ user: non_null_user,
61
+ messageboard: messageboard,
62
+ filter: messageboard.filter
63
+ )
64
+ end
65
+
66
+ private
67
+
68
+ # @return [Thredded.user_class, nil] return a user or nil if the user is a NullUser
69
+ # This is necessary because assigning a NullUser to an ActiveRecord association results in an exception.
70
+ def non_null_user
71
+ @user unless @user.thredded_anonymous?
72
+ end
73
+
74
+ def topic_categories
75
+ if category_ids
76
+ ids = category_ids.reject(&:empty?).map(&:to_i)
77
+ Category.where(id: ids)
78
+ else
79
+ []
80
+ end
81
+ end
82
+
83
+ def validate_children
84
+ promote_errors(topic.errors) if topic.invalid?
85
+ promote_errors(post.errors) if post.invalid?
86
+ end
87
+
88
+ def promote_errors(child_errors)
89
+ child_errors.each do |attribute, message|
90
+ errors.add(attribute, message)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,41 @@
1
+ module Thredded
2
+ module ApplicationHelper
3
+ # Render the page container with the supplied block as content.
4
+ def thredded_page(&block)
5
+ # enable the host app to easily check whether a thredded view is being rendered:
6
+ content_for :thredded, true
7
+ content_for :thredded_page_content, &block
8
+ render partial: 'thredded/shared/page'
9
+ end
10
+
11
+ # @param user [Thredded.user_class, Thredded::NullUser]
12
+ # @return [String] path to the user as specified by {Thredded.user_path}
13
+ def user_path(user)
14
+ Thredded.user_path(self, user)
15
+ end
16
+
17
+ # @param user [Thredded.user_class, Thredded::NullUser]
18
+ # @return [String] html_safe link to the user
19
+ def user_link(user)
20
+ render partial: 'thredded/users/link', locals: { user: user }
21
+ end
22
+
23
+ # @param datetime [DateTime]
24
+ # @return [String] html_safe datetime presentation
25
+ def time_ago(datetime)
26
+ render partial: 'thredded/shared/time_ago', locals: { datetime: datetime }
27
+ end
28
+
29
+ def paginate(collection, args = {})
30
+ super(collection, args.reverse_merge(views_prefix: 'thredded'))
31
+ end
32
+
33
+ def edit_post_path(post)
34
+ if post.private_topic_post?
35
+ edit_private_topic_private_post_path(post.postable, post)
36
+ else
37
+ edit_messageboard_topic_post_path(messageboard, post.postable, post)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ module Thredded
2
+ class ActivityUpdaterJob < ::ActiveJob::Base
3
+ queue_as :default
4
+
5
+ def perform(user_id, messageboard_id)
6
+ now = Time.zone.now
7
+
8
+ user_detail = Thredded::UserDetail.for_user_id(user_id)
9
+ user_detail.update_column(:last_seen_at, now)
10
+
11
+ Thredded::MessageboardUser
12
+ .where(
13
+ thredded_messageboard_id: messageboard_id,
14
+ thredded_user_detail_id: user_detail.id
15
+ )
16
+ .first_or_initialize
17
+ .update!(last_seen_at: now)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ module Thredded
2
+ class AtNotifierJob < ::ActiveJob::Base
3
+ queue_as :default
4
+
5
+ def perform(post_type, post_id)
6
+ post = post_type.to_s.constantize.find(post_id)
7
+
8
+ NotifyMentionedUsers.new(post).run
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Thredded
2
+ class NotifyPrivateTopicUsersJob < ::ActiveJob::Base
3
+ queue_as :default
4
+
5
+ def perform(private_topic_id)
6
+ private_topic = Thredded::PrivateTopic.find(private_topic_id)
7
+
8
+ NotifyPrivateTopicUsers.new(private_topic).run
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ module Thredded
2
+ class BaseMailer < ActionMailer::Base
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ module Thredded
2
+ class PostMailer < Thredded::BaseMailer
3
+ def at_notification(post_id, emails)
4
+ @post = Post.find(post_id)
5
+ email_details = TopicEmailDecorator.new(@post.postable)
6
+ headers['X-SMTPAPI'] = email_details.smtp_api_tag('at_notification')
7
+
8
+ mail from: email_details.no_reply,
9
+ to: email_details.no_reply,
10
+ bcc: emails,
11
+ reply_to: email_details.reply_to,
12
+ subject: email_details.subject
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Thredded
2
+ class PrivatePostMailer < Thredded::BaseMailer
3
+ def at_notification(post_id, emails)
4
+ @post = PrivatePost.find(post_id)
5
+ email_details = TopicEmailDecorator.new(@post.postable)
6
+ headers['X-SMTPAPI'] = email_details.smtp_api_tag('at_notification')
7
+
8
+ mail from: email_details.no_reply,
9
+ to: email_details.no_reply,
10
+ bcc: emails,
11
+ reply_to: email_details.reply_to,
12
+ subject: email_details.subject
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Thredded
2
+ class PrivateTopicMailer < Thredded::BaseMailer
3
+ def message_notification(private_topic_id, emails)
4
+ @topic = PrivateTopic.find(private_topic_id)
5
+ email_details = TopicEmailDecorator.new(@topic)
6
+ headers['X-SMTPAPI'] = email_details.smtp_api_tag('private_topic_mailer')
7
+
8
+ mail from: email_details.no_reply,
9
+ to: email_details.no_reply,
10
+ bcc: emails,
11
+ reply_to: email_details.reply_to,
12
+ subject: email_details.subject
13
+ end
14
+ end
15
+ end