thredded 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -10
  3. data/app/assets/images/favicons/README.md +3 -0
  4. data/app/assets/images/favicons/amazon.png +0 -0
  5. data/app/assets/images/favicons/github.png +0 -0
  6. data/app/assets/images/favicons/google_branding/logo_calendar_128px.png +0 -0
  7. data/app/assets/images/favicons/google_branding/logo_docs_48px.png +0 -0
  8. data/app/assets/images/favicons/google_branding/logo_drive_48px.png +0 -0
  9. data/app/assets/images/favicons/google_branding/logo_forms_48px.png +0 -0
  10. data/app/assets/images/favicons/google_branding/logo_sheets_48px.png +0 -0
  11. data/app/assets/images/favicons/google_branding/logo_slides_48px.png +0 -0
  12. data/app/assets/images/favicons/stackexchange.png +0 -0
  13. data/app/assets/images/favicons/twitter.png +0 -0
  14. data/app/assets/images/favicons/wikipedia.png +0 -0
  15. data/app/assets/javascripts/thredded/components/user_preferences_form.es6 +16 -1
  16. data/app/assets/stylesheets/thredded/_email.scss +52 -0
  17. data/app/assets/stylesheets/thredded/_thredded.scss +1 -0
  18. data/app/assets/stylesheets/thredded/base/_grid.scss +14 -1
  19. data/app/assets/stylesheets/thredded/base/_typography.scss +4 -0
  20. data/app/assets/stylesheets/thredded/base/_variables.scss +24 -1
  21. data/app/assets/stylesheets/thredded/components/_main-section.scss +6 -0
  22. data/app/assets/stylesheets/thredded/components/_messageboard.scss +4 -1
  23. data/app/assets/stylesheets/thredded/components/_onebox.scss +284 -0
  24. data/app/assets/stylesheets/thredded/components/_post.scss +10 -6
  25. data/app/assets/stylesheets/thredded/components/_topic-header.scss +1 -1
  26. data/app/assets/stylesheets/thredded/components/_topics.scss +5 -5
  27. data/app/assets/stylesheets/thredded/layout/_main-navigation.scss +0 -6
  28. data/app/assets/stylesheets/thredded/layout/_moderation.scss +1 -1
  29. data/app/commands/thredded/autofollow_users.rb +56 -0
  30. data/app/controllers/thredded/preferences_controller.rb +2 -0
  31. data/app/forms/thredded/user_preferences_form.rb +18 -0
  32. data/app/helpers/thredded/application_helper.rb +3 -3
  33. data/app/jobs/thredded/auto_follow_and_notify_job.rb +1 -1
  34. data/app/mailer_previews/thredded/base_mailer_preview.rb +19 -8
  35. data/app/mailers/thredded/base_mailer.rb +1 -1
  36. data/app/models/concerns/thredded/post_common.rb +2 -4
  37. data/app/models/thredded/category.rb +4 -0
  38. data/app/models/thredded/messageboard.rb +12 -6
  39. data/app/models/thredded/private_topic.rb +4 -0
  40. data/app/models/thredded/topic.rb +9 -5
  41. data/app/models/thredded/user_messageboard_preference.rb +24 -0
  42. data/app/models/thredded/user_preference.rb +2 -0
  43. data/app/models/thredded/user_topic_follow.rb +1 -1
  44. data/app/notifiers/thredded/email_notifier.rb +1 -15
  45. data/app/views/thredded/messageboard_groups/new.html.erb +15 -13
  46. data/app/views/thredded/messageboards/_form.html.erb +22 -22
  47. data/app/views/thredded/messageboards/edit.html.erb +3 -1
  48. data/app/views/thredded/messageboards/new.html.erb +3 -1
  49. data/app/views/thredded/moderation/_post.html.erb +1 -1
  50. data/app/views/thredded/moderation/_user_post.html.erb +1 -1
  51. data/app/views/thredded/moderation/activity.html.erb +3 -3
  52. data/app/views/thredded/moderation/history.html.erb +2 -2
  53. data/app/views/thredded/moderation/pending.html.erb +2 -2
  54. data/app/views/thredded/moderation/user.html.erb +43 -41
  55. data/app/views/thredded/moderation/users.html.erb +32 -30
  56. data/app/views/thredded/post_mailer/post_notification.html.erb +21 -12
  57. data/app/views/thredded/posts/_content.html.erb +1 -1
  58. data/app/views/thredded/posts_common/_content.html.erb +1 -3
  59. data/app/views/thredded/preferences/_form.html.erb +25 -8
  60. data/app/views/thredded/private_posts/_content.html.erb +1 -1
  61. data/app/views/thredded/private_topic_mailer/message_notification.html.erb +17 -14
  62. data/app/views/thredded/private_topics/edit.html.erb +21 -19
  63. data/app/views/thredded/topics/edit.html.erb +32 -30
  64. data/app/views/thredded/topics/new.html.erb +8 -6
  65. data/app/views/thredded/topics/show.html.erb +1 -1
  66. data/app/views/thredded/users/_post.html.erb +1 -1
  67. data/bin/rubocop +17 -0
  68. data/config/locales/en.yml +13 -1
  69. data/config/locales/es.yml +14 -0
  70. data/config/locales/pl.yml +13 -0
  71. data/config/locales/pt-BR.yml +12 -0
  72. data/config/locales/ru.yml +197 -0
  73. data/db/migrate/20160329231848_create_thredded.rb +2 -8
  74. data/db/upgrade_migrations/20161113161801_upgrade_v0_8_to_v0_9.rb +6 -5
  75. data/db/upgrade_migrations/20170312131417_upgrade_thredded_v0_10_to_v0_11.rb +20 -0
  76. data/lib/generators/thredded/install/templates/initializer.rb +12 -0
  77. data/lib/thredded.rb +12 -3
  78. data/lib/thredded/content_formatter.rb +16 -25
  79. data/lib/thredded/email_transformer.rb +21 -0
  80. data/lib/thredded/email_transformer/base.rb +47 -0
  81. data/lib/thredded/email_transformer/onebox.rb +20 -0
  82. data/lib/thredded/formatting_demo_content.rb +29 -0
  83. data/lib/thredded/html_pipeline/kramdown_filter.rb +5 -1
  84. data/lib/thredded/html_pipeline/onebox_filter.rb +136 -0
  85. data/lib/thredded/version.rb +1 -1
  86. metadata +62 -22
  87. data/app/commands/thredded/autofollow_mentioned_users.rb +0 -31
  88. data/app/commands/thredded/members_marked_notified.rb +0 -19
  89. data/app/models/thredded/post_notification.rb +0 -18
@@ -1,13 +1,14 @@
1
1
  .thredded--post {
2
2
  position: relative;
3
- margin-bottom: $thredded-large-spacing;
4
- @include thredded-media-mobile {
5
- margin-bottom: $thredded-base-spacing;
3
+ margin-bottom: $thredded-base-spacing;
4
+ @include thredded-media-desktop-and-up {
5
+ margin-bottom: $thredded-large-spacing;
6
6
  }
7
7
  }
8
8
 
9
9
  .thredded--post--dropdown--toggle {
10
10
  fill: currentColor;
11
+ box-sizing: content-box;
11
12
  color: $thredded-action-color;
12
13
  display: inline-block;
13
14
  width: 1rem;
@@ -15,6 +16,9 @@
15
16
  padding: 0.875rem 0.875rem 0.875rem 1.5rem;
16
17
  margin-right: -0.875rem;
17
18
  -webkit-tap-highlight-color: transparent;
19
+ @include thredded-media-content-breakout {
20
+ margin-top: -0.75rem;
21
+ }
18
22
  }
19
23
 
20
24
  .thredded--post--dropdown {
@@ -69,8 +73,8 @@ form.button_to > .thredded--post--dropdown--actions--item {
69
73
  &:active,
70
74
  &:focus,
71
75
  &:hover {
72
- background-color: $thredded-action-color;
73
- color: $thredded-background-color;
76
+ background-color: $thredded-button-background;
77
+ color: $thredded-button-color;
74
78
  text-decoration: none;
75
79
  cursor: pointer;
76
80
  }
@@ -100,7 +104,7 @@ form.button_to > .thredded--post--dropdown--actions--item {
100
104
  vertical-align: baseline;
101
105
  width: 1.75rem; // 28px
102
106
 
103
- @include thredded-media-avatar-breakout {
107
+ @include thredded-media-content-breakout {
104
108
  height: 2.25rem; // 36px
105
109
  left: -3rem;
106
110
  position: absolute;
@@ -2,7 +2,7 @@
2
2
  margin-bottom: $thredded-small-spacing;
3
3
  margin-top: 0;
4
4
  @include thredded-media-desktop-and-up {
5
- margin-bottom: $thredded-large-spacing;
5
+ margin-bottom: $thredded-base-spacing + $thredded-small-spacing;
6
6
  margin-top: $thredded-small-spacing;
7
7
  @include thredded--clearfix;
8
8
  }
@@ -2,11 +2,11 @@
2
2
  margin-bottom: $thredded-topics-list-gutter-y;
3
3
  position: relative;
4
4
 
5
- @media (max-width: $thredded-grid-container-max-width) {
6
- margin-left: 3rem;
5
+ @include thredded-media-content-no-breakout {
6
+ margin-left: $thredded-topics-topic-posts-counter-width;
7
7
  }
8
8
  @include thredded-media-mobile {
9
- margin-right: 2rem;
9
+ margin-right: $thredded-topics-topic-follow-icon-width;
10
10
  }
11
11
  }
12
12
 
@@ -115,7 +115,7 @@
115
115
  font-weight: 900;
116
116
  font-size: 0.8rem;
117
117
  height: 2rem;
118
- left: -3rem;
118
+ left: -$thredded-topics-topic-posts-counter-width;
119
119
  line-height: 2rem;
120
120
  margin-right: $thredded-base-spacing;
121
121
  position: absolute;
@@ -150,7 +150,7 @@
150
150
  right: -1.6rem;
151
151
  top: 0;
152
152
  @include thredded-media-mobile {
153
- right: -2rem;
153
+ right: -$thredded-topics-topic-posts-counter-width;
154
154
  }
155
155
  }
156
156
 
@@ -68,12 +68,6 @@
68
68
  display: none;
69
69
  }
70
70
  }
71
-
72
- @include thredded-media-desktop-and-up {
73
- a, &.thredded--navigation-breadcrumbs--item-no-link {
74
- padding: $thredded-base-spacing 0;
75
- }
76
- }
77
71
  }
78
72
 
79
73
  a {
@@ -42,7 +42,7 @@
42
42
  .thredded--post {
43
43
  margin-bottom: 0;
44
44
  margin-left: 1rem;
45
- @include thredded-media-avatar-breakout {
45
+ @include thredded-media-content-breakout {
46
46
  margin-left: 4rem;
47
47
  }
48
48
  }
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ module Thredded
3
+ class AutofollowUsers
4
+ def initialize(post)
5
+ @post = post
6
+ end
7
+
8
+ def run
9
+ new_followers.each do |user, reason|
10
+ Thredded::UserTopicFollow.create_unless_exists(user.id, post.postable_id, reason)
11
+ end
12
+ end
13
+
14
+ # @return [Array<Thredded.user_class>]
15
+ def mentioned_users
16
+ @mentioned_users ||= Thredded::AtNotificationExtractor.new(post).run
17
+ end
18
+
19
+ # @return [Hash<Thredded.user_class, Symbol]>] a map of users that should get subscribed to their the follow reason.
20
+ def new_followers
21
+ result = {}
22
+ auto_followers.each { |user| result[user] = :auto }
23
+ exclude_follow_on_mention_opt_outs(mentioned_users).each { |user| result[user] = :mentioned }
24
+ result.delete(post.user)
25
+ result
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :post
31
+
32
+ # Returns the users that have:
33
+ # UserMessageboardPreference#auto_follow_topics? && UserPreference#auto_follow_topics?
34
+ # If the `user_preference` for a given does not exist, its default value is used.
35
+ # @return [Enumerable<Thredded.user_class>]
36
+ def auto_followers
37
+ user_board_prefs = post.messageboard.user_messageboard_preferences.each_with_object({}) do |ump, h|
38
+ h[ump.user] = ump
39
+ end
40
+ Thredded.user_class.includes(:thredded_user_preference)
41
+ .select(Thredded.user_class.primary_key)
42
+ .find_each(batch_size: 50_000).select do |user|
43
+ (user_board_prefs[user] ||
44
+ Thredded::UserMessageboardPreference.new(messageboard: post.messageboard, user: user)).auto_follow_topics?
45
+ end
46
+ end
47
+
48
+ # @return [Enumerable<Thredded.user_class>]
49
+ def exclude_follow_on_mention_opt_outs(users)
50
+ users.select do |user|
51
+ user.thredded_user_preference.follow_topics_on_mention? &&
52
+ user.thredded_user_messageboard_preferences.in(post.messageboard).follow_topics_on_mention?
53
+ end
54
+ end
55
+ end
56
+ end
@@ -27,6 +27,8 @@ module Thredded
27
27
 
28
28
  def preferences_params
29
29
  params.fetch(:user_preferences_form, {}).permit(
30
+ :auto_follow_topics,
31
+ :messageboard_auto_follow_topics,
30
32
  :follow_topics_on_mention,
31
33
  :messageboard_follow_topics_on_mention,
32
34
  messageboard_notifications_for_followed_topics_attributes: %i(notifier_key id messageboard_id enabled),
@@ -9,12 +9,14 @@ module Thredded
9
9
  validate :validate_children
10
10
 
11
11
  delegate :follow_topics_on_mention, :follow_topics_on_mention=,
12
+ :auto_follow_topics, :auto_follow_topics=,
12
13
  :messageboard_notifications_for_followed_topics_attributes=,
13
14
  :notifications_for_followed_topics_attributes=,
14
15
  :notifications_for_private_topics_attributes=,
15
16
  to: :user_preference
16
17
 
17
18
  delegate :follow_topics_on_mention, :follow_topics_on_mention=,
19
+ :auto_follow_topics, :auto_follow_topics=,
18
20
  to: :user_messageboard_preference,
19
21
  prefix: :messageboard
20
22
 
@@ -31,6 +33,14 @@ module Thredded
31
33
  return false unless valid?
32
34
  Thredded::UserPreference.transaction do
33
35
  user_preference.save!
36
+
37
+ # Update all of the messageboards' auto_follow_topics if the global preference has changed.
38
+ if user_preference.previous_changes.include?('auto_follow_topics')
39
+ UserMessageboardPreference.where(user: @user)
40
+ .update_all(auto_follow_topics: user_preference.auto_follow_topics)
41
+ user_messageboard_preference.auto_follow_topics_will_change! if messageboard
42
+ end
43
+
34
44
  user_messageboard_preference.save! if messageboard
35
45
  end
36
46
  true
@@ -55,6 +65,14 @@ module Thredded
55
65
  end
56
66
  end
57
67
 
68
+ def update_path
69
+ if @messageboard
70
+ Thredded::UrlsHelper.messageboard_preferences_path(@messageboard)
71
+ else
72
+ Thredded::UrlsHelper.global_preferences_path
73
+ end
74
+ end
75
+
58
76
  private
59
77
 
60
78
  # @return [Thredded::UserPreference]
@@ -19,7 +19,7 @@ module Thredded
19
19
 
20
20
  def thredded_container_classes
21
21
  ['thredded--main-container', content_for(:thredded_page_id)].tap do |classes|
22
- classes << 'thredded--is-moderator' if moderatable_messageboards_ids
22
+ classes << 'thredded--is-moderator' unless moderatable_messageboards_ids.empty?
23
23
  end
24
24
  end
25
25
 
@@ -93,11 +93,11 @@ module Thredded
93
93
  ]
94
94
  end
95
95
 
96
- # @param follow_reason ['manual', 'posted', 'mentioned', nil]
96
+ # @param follow_reason ['manual', 'posted', 'mentioned', 'auto', nil]
97
97
  def topic_follow_reason_text(follow_reason)
98
98
  if follow_reason
99
99
  # rubocop:disable Metrics/LineLength
100
- # i18n-tasks-use t('thredded.topics.following.manual') t('thredded.topics.following.posted') t('thredded.topics.following.mentioned')
100
+ # i18n-tasks-use t('thredded.topics.following.manual') t('thredded.topics.following.posted') t('thredded.topics.following.mentioned') t('thredded.topics.following.auto')
101
101
  # rubocop:enable Metrics/LineLength
102
102
  t("thredded.topics.following.#{follow_reason}")
103
103
  else
@@ -6,7 +6,7 @@ module Thredded
6
6
  def perform(post_id)
7
7
  post = Post.find(post_id)
8
8
 
9
- AutofollowMentionedUsers.new(post).run
9
+ AutofollowUsers.new(post).run
10
10
  NotifyFollowingUsers.new(post).run
11
11
  end
12
12
  end
@@ -11,18 +11,22 @@ module Thredded
11
11
 
12
12
  def mock_content(mention_users: [])
13
13
  <<-MARKDOWN
14
- #{mention_users.map { |u| "@#{u}" } * ', '}, if we synthesize the driver, we can get to the HDD panel through the `1080p EXE` bus!
15
- I'll program the **redundant** SMTP array, that should monitor the SMS microchip!
16
- MARKDOWN
14
+ Hey #{mention_users.map { |u| "@#{u}" } * ', '}!
15
+ All of the basic [Markdown](https://kramdown.gettalong.org/quickref.html) formatting is supported (powered by [Kramdown](https://kramdown.gettalong.org)).
16
+
17
+ Additionally, Markdown is extended to support the following:
18
+
19
+ #{Thredded::FormattingDemoContent.parts.join("\n")}
20
+ MARKDOWN
17
21
  end
18
22
 
19
23
  def mock_topic(attr = {})
24
+ fail 'Do not assign ID here or a has_many association might get updated' if attr.key?(:id)
20
25
  Topic.new(
21
26
  attr.reverse_merge(
22
27
  title: 'A test topic',
23
28
  slug: 'a-test-topic',
24
29
  created_at: 3.days.ago,
25
- id: 1 + rand(1334),
26
30
  last_user: mock_user,
27
31
  locked: [false, true].sample,
28
32
  messageboard: mock_messageboard,
@@ -46,16 +50,16 @@ MARKDOWN
46
50
  updated_at: Time.zone.now,
47
51
  user: topic.last_user,
48
52
  )
49
- )
53
+ ).tap { |m| mock_post_cache_key! m }
50
54
  end
51
55
 
52
56
  def mock_private_topic(attr = {})
57
+ fail 'Do not assign ID here or a has_many association might get updated' if attr.key?(:id)
53
58
  PrivateTopic.new(
54
59
  attr.reverse_merge(
55
60
  title: 'A test private topic',
56
61
  slug: 'a-test-private-topic',
57
62
  created_at: 3.days.ago,
58
- id: 1 + rand(1334),
59
63
  last_user: mock_user,
60
64
  posts_count: 1 + rand(42),
61
65
  updated_at: Time.zone.now,
@@ -75,17 +79,17 @@ MARKDOWN
75
79
  updated_at: Time.zone.now,
76
80
  user: private_topic.last_user,
77
81
  )
78
- )
82
+ ).tap { |m| mock_post_cache_key! m }
79
83
  end
80
84
 
81
85
  def mock_messageboard(attr = {})
86
+ fail 'Do not assign ID here or a has_many association might get updated' if attr.key?(:id)
82
87
  Messageboard.new(
83
88
  attr.reverse_merge(
84
89
  name: 'A test messageboard',
85
90
  slug: 'a-test-messageboard',
86
91
  description: 'Test messageboard description',
87
92
  created_at: 1.month.ago,
88
- id: 1 + rand(1334),
89
93
  posts_count: rand(1337),
90
94
  topics_count: rand(42),
91
95
  updated_at: Time.zone.now,
@@ -102,5 +106,12 @@ MARKDOWN
102
106
  )
103
107
  )
104
108
  end
109
+
110
+ def mock_post_cache_key!(post)
111
+ orig_key = post.cache_key
112
+ post.define_singleton_method :cache_key do
113
+ orig_key.sub(/new$/, "preview-#{Digest.hexencode(Digest::SHA2.new.digest(content))}")
114
+ end
115
+ end
105
116
  end
106
117
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Thredded
3
- class BaseMailer < ActionMailer::Base
3
+ class BaseMailer < Thredded.parent_mailer.constantize
4
4
  helper ::Thredded::UrlsHelper
5
5
 
6
6
  protected
@@ -12,8 +12,6 @@ module Thredded
12
12
 
13
13
  delegate :email, to: :user, prefix: true, allow_nil: true
14
14
 
15
- has_many :post_notifications, as: :post, dependent: :destroy
16
-
17
15
  validates :content, presence: true
18
16
 
19
17
  scope :order_oldest_first, -> { order(created_at: :asc, id: :asc) }
@@ -32,8 +30,8 @@ module Thredded
32
30
 
33
31
  # @param view_context [Object] the context of the rendering view.
34
32
  # @return [String] formatted and sanitized html-safe post content.
35
- def filtered_content(view_context, users_provider: ->(names) { readers_from_user_names(names) })
36
- Thredded::ContentFormatter.new(view_context, users_provider: users_provider).format_content(content)
33
+ def filtered_content(view_context, users_provider: ->(names) { readers_from_user_names(names) }, **options)
34
+ Thredded::ContentFormatter.new(view_context, users_provider: users_provider, **options).format_content(content)
37
35
  end
38
36
 
39
37
  def first_post_in_topic?
@@ -9,5 +9,9 @@ module Thredded
9
9
 
10
10
  validates :name, presence: true
11
11
  validates :messageboard_id, presence: true
12
+
13
+ def normalize_friendly_id(input)
14
+ Thredded.slugifier.call(input.to_s)
15
+ end
12
16
  end
13
17
  end
@@ -96,17 +96,23 @@ module Thredded
96
96
  last_topic.try(:last_user)
97
97
  end
98
98
 
99
+ def update_last_topic!
100
+ return if destroyed?
101
+ self.last_topic = topics.order_recently_posted_first.moderation_state_visible_to_all.first
102
+ save! if last_topic_id_changed?
103
+ end
104
+
105
+ def normalize_friendly_id(input)
106
+ Thredded.slugifier.call(input.to_s)
107
+ end
108
+
109
+ private
110
+
99
111
  def slug_candidates
100
112
  [
101
113
  :name,
102
114
  [:name, '-board']
103
115
  ]
104
116
  end
105
-
106
- def update_last_topic!
107
- return if destroyed?
108
- self.last_topic = topics.order_recently_posted_first.moderation_state_visible_to_all.first
109
- save! if last_topic_id_changed?
110
- end
111
117
  end
112
118
  end
@@ -66,6 +66,10 @@ module Thredded
66
66
  title_changed?
67
67
  end
68
68
 
69
+ def normalize_friendly_id(input)
70
+ Thredded.slugifier.call(input.to_s)
71
+ end
72
+
69
73
  private
70
74
 
71
75
  def slug_candidates
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Thredded
3
- class Topic < ActiveRecord::Base
3
+ class Topic < ActiveRecord::Base # rubocop:disable Metrics/ClassLength
4
4
  include Thredded::TopicCommon
5
5
  include Thredded::ContentModerationState
6
6
 
@@ -121,10 +121,6 @@ module Thredded
121
121
  true
122
122
  end
123
123
 
124
- def should_generate_new_friendly_id?
125
- title_changed?
126
- end
127
-
128
124
  # @return [Thredded::PostModerationRecord, nil]
129
125
  def last_moderation_record
130
126
  first_post.try(:last_moderation_record)
@@ -144,6 +140,14 @@ module Thredded
144
140
  end
145
141
  end
146
142
 
143
+ def should_generate_new_friendly_id?
144
+ title_changed?
145
+ end
146
+
147
+ def normalize_friendly_id(input)
148
+ Thredded.slugifier.call(input.to_s)
149
+ end
150
+
147
151
  private
148
152
 
149
153
  def slug_candidates