thredded 0.16.0 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -3
  3. data/app/assets/javascripts/thredded/components/topics.es6 +27 -6
  4. data/app/assets/javascripts/thredded/components/users_select.es6 +6 -3
  5. data/app/assets/stylesheets/thredded/base/_variables.scss +4 -1
  6. data/app/assets/stylesheets/thredded/components/_messageboard.scss +6 -1
  7. data/app/assets/stylesheets/thredded/components/_topic-header.scss +2 -2
  8. data/app/assets/stylesheets/thredded/layout/_user-navigation.scss +7 -0
  9. data/app/controllers/thredded/autocomplete_users_controller.rb +3 -1
  10. data/app/controllers/thredded/moderation_controller.rb +5 -5
  11. data/app/controllers/thredded/private_topics_controller.rb +2 -2
  12. data/app/controllers/thredded/theme_previews_controller.rb +9 -3
  13. data/app/controllers/thredded/topics_controller.rb +19 -6
  14. data/app/helpers/thredded/application_helper.rb +1 -0
  15. data/app/helpers/thredded/icon_helper.rb +45 -0
  16. data/app/models/concerns/thredded/content_moderation_state.rb +1 -1
  17. data/app/models/concerns/thredded/topic_common.rb +6 -11
  18. data/app/models/concerns/thredded/user_topic_read_state_common.rb +5 -0
  19. data/app/models/thredded/null_user.rb +5 -0
  20. data/app/models/thredded/null_user_topic_read_state.rb +6 -0
  21. data/app/models/thredded/private_topic.rb +18 -0
  22. data/app/models/thredded/topic.rb +20 -6
  23. data/app/view_models/thredded/base_topic_view.rb +7 -8
  24. data/app/view_models/thredded/messageboard_group_view.rb +9 -2
  25. data/app/view_models/thredded/messageboard_view.rb +17 -3
  26. data/app/view_models/thredded/topic_view.rb +4 -4
  27. data/app/views/thredded/messageboards/_grid_sizers.html.erb +2 -0
  28. data/app/views/thredded/messageboards/index.html.erb +2 -4
  29. data/app/views/thredded/messageboards/messageboard/_meta.html.erb +1 -1
  30. data/app/views/thredded/messageboards/messageboard/_unread_followed_topics_badge.html.erb +1 -1
  31. data/app/views/thredded/private_topics/index.html.erb +3 -2
  32. data/app/views/thredded/shared/nav/_unread_topics.html.erb +3 -2
  33. data/app/views/thredded/theme_previews/show.html.erb +1 -4
  34. data/app/views/thredded/topics/_form.html.erb +2 -0
  35. data/app/views/thredded/topics/_topic.html.erb +11 -8
  36. data/app/views/thredded/topics/index.html.erb +7 -7
  37. data/app/views/thredded/topics/unread.html.erb +5 -5
  38. data/db/migrate/20160329231848_create_thredded.rb +2 -2
  39. data/lib/generators/thredded/install/templates/initializer.rb +8 -2
  40. data/lib/thredded/version.rb +1 -1
  41. metadata +6 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a642798592ef42f04c27440bdeb2b9930273fca1bbb2cb41da3138dd10af429c
4
- data.tar.gz: 78e334145b31fc55950554b3909864fbe6617a262fbfa07006eec2262ab526bf
3
+ metadata.gz: aab83b50d26733e85b80ff5a6c6373c53ddc48b93ac14de35212bdea46ff3330
4
+ data.tar.gz: a02d079d68535ed32160d9e2d6299dc96918bbda7c694212441cf51091f367e3
5
5
  SHA512:
6
- metadata.gz: 5b134c56f3c79b38ed2b13b42e45c6c5fa7dbb54258ab941980fa7791077a8f1fdc1e76ba24d0c5658478ce762beddaf080133fe16fbd8684c467f2d1efbbe80
7
- data.tar.gz: 560bfe2c2922595f5a93728bffb7becb25a32a39fa4ddc209ff8f5778c28ccc87cd46b0a08d106383cf2bb440c83f1a7a532bd0247683ff2e63d30028ca99e3f
6
+ metadata.gz: 15f36babaaacfeeee4afc75863097638c919bb94bd6bd27501e0c4ebddb851bd45af5a664733ec8a823e1d39598f86bfeafe15eceae7d46942459f32d02e04d0
7
+ data.tar.gz: c6d79b3effb4bd22ba0b4dfe6e434d55ee445c5237b0e00f5d651a400e48d2c3a6c707503bf8f8803e54b170fef6c44f3c7fb5d131c4357aec1b7e5496b0beea
data/README.md CHANGED
@@ -95,7 +95,7 @@ Then, see the rest of this Readme for more information about using and customizi
95
95
  Add the gem to your Gemfile:
96
96
 
97
97
  ```ruby
98
- gem 'thredded', '~> 0.16.0'
98
+ gem 'thredded', '~> 0.16.1'
99
99
  ```
100
100
 
101
101
  Add the Thredded [initializer] to your parent app by running the install generator.
@@ -559,13 +559,20 @@ Rails.application.config.to_prepare do
559
559
  Thredded::ApplicationController.module_eval do
560
560
  # Require authentication to access the forums:
561
561
  before_action :thredded_require_login!
562
+ # NB: in rails 4.2 you will need to change this to:
563
+ # before_action { thredded_require_login! }
562
564
 
563
565
  # You may also want to render a login form after the
564
566
  # "Please sign in first" message:
565
567
  rescue_from Thredded::Errors::LoginRequired do |exception|
566
568
  # Place the code for rendering the login form here, for example:
567
- @message = exception.message
568
- render template: 'sessions/new', status: :forbidden
569
+ flash.now[:notice] = exception.message
570
+ controller = Users::SessionsController.new
571
+ controller.request = request
572
+ controller.request.env['devise.mapping'] = Devise.mappings[:user]
573
+ controller.response = response
574
+ controller.response_options = { status: :forbidden }
575
+ controller.process(:new)
569
576
  end
570
577
  end
571
578
  end
@@ -10,32 +10,53 @@
10
10
  const TOPIC_UNREAD_CLASS = 'thredded--topic-unread';
11
11
  const TOPIC_READ_CLASS = 'thredded--topic-read';
12
12
  const POSTS_COUNT_SELECTOR = '.thredded--topics--posts-count';
13
- const POSTS_PER_PAGE = 50;
14
13
 
15
14
  function pageNumber(url) {
16
15
  const match = url.match(/\/page-(\d)$/);
17
16
  return match ? +match[1] : 1;
18
17
  }
19
18
 
20
- function totalPages(numPosts) {
21
- return Math.ceil(numPosts / POSTS_PER_PAGE);
19
+ function totalPages(numPosts, postsPerPage) {
20
+ return Math.ceil(numPosts / postsPerPage);
22
21
  }
23
22
 
24
- function getTopicNode(node) {
23
+ function getAncestorTag(node, ancestorTagName) {
25
24
  do {
26
25
  node = node.parentNode;
27
- } while (node && node.tagName !== 'ARTICLE');
26
+ } while (node && node.tagName !== ancestorTagName);
28
27
  return node;
29
28
  }
30
29
 
30
+ function getTopicNode(node) {
31
+ return getAncestorTag(node, 'ARTICLE');
32
+ }
33
+
34
+ function getUnreadNavItem(unreadFollowedCountElement) {
35
+ return getAncestorTag(unreadFollowedCountElement, 'LI');
36
+ }
37
+
31
38
  function initTopicsList(topicsList) {
39
+ const postsPerPage = +topicsList.getAttribute('data-thredded-topic-posts-per-page') || 25;
40
+ const isPrivateTopics = topicsList.getAttribute('data-thredded-topics') === 'private';
41
+ const unreadFollowedCountElement = document.querySelector('[data-unread-followed-count]');
32
42
  topicsList.addEventListener('click', (evt) => {
33
43
  const link = evt.target;
34
44
  if (link.tagName !== 'A' || link.parentNode.tagName !== 'H1') return;
35
45
  const topic = getTopicNode(link);
36
- if (pageNumber(link.href) === totalPages(+topic.querySelector(POSTS_COUNT_SELECTOR).textContent)) {
46
+ if (pageNumber(link.href) === totalPages(+topic.querySelector(POSTS_COUNT_SELECTOR).textContent, postsPerPage)) {
47
+ if (!isPrivateTopics && unreadFollowedCountElement &&
48
+ topic.hasAttribute('data-followed') && topic.hasAttribute('data-unread')) {
49
+ const count = (+unreadFollowedCountElement.textContent) - 1;
50
+ if (count === 0) {
51
+ const navItem = getUnreadNavItem(unreadFollowedCountElement);
52
+ navItem.parentElement.removeChild(navItem);
53
+ } else {
54
+ unreadFollowedCountElement.textContent = count.toLocaleString();
55
+ }
56
+ }
37
57
  topic.classList.add(TOPIC_READ_CLASS);
38
58
  topic.classList.remove(TOPIC_UNREAD_CLASS);
59
+ topic.removeAttribute('data-unread');
39
60
  }
40
61
  });
41
62
  }
@@ -44,7 +44,7 @@
44
44
  current.push(char);
45
45
  }
46
46
  }
47
- if (current.length) result.push({name: current.join(''), index: currentIndex});
47
+ if (current.length) result.current = {name: current.join(''), index: currentIndex};
48
48
  return result;
49
49
  }
50
50
 
@@ -63,6 +63,9 @@
63
63
  maxCount: Thredded.UsersSelect.DROPDOWN_MAX_COUNT,
64
64
  },
65
65
  });
66
+ textarea.addEventListener('blur', (evt) => {
67
+ textcomplete.hide();
68
+ });
66
69
 
67
70
  const searchFn = Thredded.UserTextcomplete.searchFn({
68
71
  url: textarea.getAttribute('data-autocomplete-url'),
@@ -77,8 +80,8 @@
77
80
  index: 0,
78
81
  match: (text) => {
79
82
  const names = parseNames(text);
80
- if (names.length) {
81
- const {name, index} = names[names.length - 1];
83
+ if (names.current) {
84
+ const {name, index} = names.current;
82
85
  const matchData = [name];
83
86
  matchData.index = index;
84
87
  return matchData;
@@ -7,12 +7,15 @@ $thredded-grid-breakpoint-max-widths: (mobile: 34rem, tablet: 48rem) !default;
7
7
  $thredded-content-breakout-min-width: 4rem !default;
8
8
 
9
9
  // Typography
10
- $thredded-base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif !default;
10
+ $thredded-base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
11
11
  $thredded-base-font-size: 1rem !default; // 16px
12
12
  $thredded-font-size-small: 0.875rem !default; // 14px
13
13
  $thredded-base-line-height: 1.5 !default;
14
14
  $thredded-heading-font-family: inherit !default;
15
15
  $thredded-heading-line-height: 1.2 !default;
16
+ $thredded-messageboard-title-font-size: 1.125rem !default;
17
+ $thredded-topic-header-font-size: 1.25rem !default;
18
+ $thredded-topic-header-font-size-tablet-and-up: 1.5rem !default;
16
19
 
17
20
  // Spacings
18
21
  $thredded-large-spacing: $thredded-base-line-height * 2rem !default;
@@ -29,9 +29,9 @@
29
29
 
30
30
  .thredded--messageboard--title {
31
31
  @extend %thredded--heading;
32
+ font-size: $thredded-messageboard-title-font-size;
32
33
  display: inline-block;
33
34
  float: left;
34
- font-size: 1.125rem; // 18px
35
35
  line-height: 1.2;
36
36
  vertical-align: baseline;
37
37
  }
@@ -157,6 +157,11 @@
157
157
  }
158
158
  }
159
159
 
160
+ // A helper class for sizing incomplete rows with more than two missing items.
161
+ .thredded--grid-sizer {
162
+ @extend %thredded--messageboards-cell-flex;
163
+ }
164
+
160
165
  .thredded--messageboard {
161
166
  @extend %thredded--messageboards-cell-flex;
162
167
  margin-top: $margin-y;
@@ -10,11 +10,11 @@
10
10
 
11
11
  .thredded--topic-header--title {
12
12
  @extend %thredded--heading;
13
- font-size: 1.25rem;
13
+ font-size: $thredded-topic-header-font-size;
14
14
  line-height: 1.2;
15
15
  margin-bottom: $thredded-small-spacing / 2;
16
16
  @include thredded-media-tablet-and-up {
17
- font-size: 1.5rem;
17
+ font-size: $thredded-topic-header-font-size-tablet-and-up;
18
18
  }
19
19
  }
20
20
 
@@ -20,3 +20,10 @@
20
20
  .thredded--user-navigation--unread-topics--followed-count {
21
21
  @extend %thredded--nav-tabs--item--badge;
22
22
  }
23
+ .thredded--unread-topics--followed-icon {
24
+ fill: currentColor;
25
+ width: 1em;
26
+ height: 1em;
27
+ top: 0.1em;
28
+ position: relative;
29
+ }
@@ -28,8 +28,10 @@ module Thredded
28
28
  def users_by_prefix
29
29
  query = params[:q].to_s.strip
30
30
  if query.length >= Thredded.autocomplete_min_length
31
- DbTextSearch::CaseInsensitive.new(users_scope, Thredded.user_name_column).prefix(query)
31
+ case_insensitive = DbTextSearch::CaseInsensitive.new(users_scope, Thredded.user_name_column)
32
+ case_insensitive.prefix(query)
32
33
  .where.not(id: thredded_current_user.id)
34
+ .order(case_insensitive.column_for_order(:asc))
33
35
  .limit(MAX_RESULTS)
34
36
  else
35
37
  []
@@ -12,7 +12,7 @@ module Thredded
12
12
  .pending_moderation
13
13
  .order_oldest_first
14
14
  .preload(:user, :postable)
15
- .page(current_page)
15
+ .send(Kaminari.config.page_method_name, current_page)
16
16
  )
17
17
  maybe_set_last_moderated_record_flash
18
18
  end
@@ -20,7 +20,7 @@ module Thredded
20
20
  def history
21
21
  @post_moderation_records = accessible_post_moderation_records
22
22
  .order(created_at: :desc)
23
- .page(current_page)
23
+ .send(Kaminari.config.page_method_name, current_page)
24
24
  end
25
25
 
26
26
  def activity
@@ -29,7 +29,7 @@ module Thredded
29
29
  moderatable_posts
30
30
  .order_newest_first
31
31
  .preload(:user, :postable, :messageboard)
32
- .page(current_page)
32
+ .send(Kaminari.config.page_method_name, current_page)
33
33
  )
34
34
  maybe_set_last_moderated_record_flash
35
35
  end
@@ -63,7 +63,7 @@ module Thredded
63
63
  )
64
64
  @query = params[:q].to_s
65
65
  @users = DbTextSearch::CaseInsensitive.new(@users, Thredded.user_name_column).prefix(@query) if @query.present?
66
- @users = @users.page(current_page)
66
+ @users = @users.send(Kaminari.config.page_method_name, current_page)
67
67
  end
68
68
 
69
69
  def user
@@ -73,7 +73,7 @@ module Thredded
73
73
  .where(messageboard_id: policy_scope(Messageboard.all).pluck(:id))
74
74
  .order_newest_first
75
75
  .includes(:postable)
76
- .page(current_page)
76
+ .send(Kaminari.config.page_method_name, current_page)
77
77
  @posts = Thredded::PostsPageView.new(thredded_current_user, posts_scope)
78
78
  end
79
79
 
@@ -12,7 +12,7 @@ module Thredded
12
12
  .distinct
13
13
  .for_user(thredded_current_user)
14
14
  .order_recently_posted_first
15
- .page(params[:page])
15
+ .send(Kaminari.config.page_method_name, params[:page])
16
16
  return redirect_to(last_page_params(page_scope)) if page_beyond_last?(page_scope)
17
17
  @private_topics = Thredded::PrivateTopicsPageView.new(thredded_current_user, page_scope)
18
18
 
@@ -29,7 +29,7 @@ module Thredded
29
29
  .posts
30
30
  .includes(:user)
31
31
  .order_oldest_first
32
- .page(current_page)
32
+ .send(Kaminari.config.page_method_name, current_page)
33
33
  return redirect_to(last_page_params(page_scope)) if page_beyond_last?(page_scope)
34
34
  @posts = Thredded::TopicPostsPageView.new(thredded_current_user, private_topic, page_scope)
35
35
  Thredded::UserPrivateTopicReadState.touch!(thredded_current_user.id, page_scope.last) if thredded_signed_in?
@@ -15,11 +15,15 @@ module Thredded
15
15
  Thredded::MessageboardView.new(Thredded::Messageboard.first(2)[-1], unread_topics_count: 2),
16
16
  Thredded::MessageboardView.new(Thredded::Messageboard.first(3)[-1]),
17
17
  ]
18
- @topics = Thredded::TopicsPageView.new(@user, @messageboard.topics.page(1).limit(3))
18
+ @topics = Thredded::TopicsPageView.new(@user, @messageboard.topics
19
+ .send(Kaminari.config.page_method_name, 1)
20
+ .limit(3))
19
21
  @private_topics = Thredded::PrivateTopicsPageView.new(@user, @user.thredded_private_topics.page(1).limit(3))
20
22
  topic = Thredded::Topic.new(messageboard: @messageboard, title: 'Hello', slug: 'hello', user: @user)
21
23
  @topic = Thredded::TopicView.from_user(topic, @user)
22
- @posts = Thredded::TopicPostsPageView.new(@user, topic, topic.posts.page(1).limit(3))
24
+ @posts = Thredded::TopicPostsPageView.new(@user, topic, topic.posts
25
+ .send(Kaminari.config.page_method_name, 1)
26
+ .limit(3))
23
27
  @post = topic.posts.build(id: 1337, postable: topic, content: 'Hello world', user: @user)
24
28
  @post_form = Thredded::PostForm.for_persisted(@post)
25
29
  @new_post = Thredded::PostForm.new(user: @user, topic: topic)
@@ -27,7 +31,9 @@ module Thredded
27
31
  @new_private_topic = Thredded::PrivateTopicForm.new(user: @user)
28
32
  private_topic = Thredded::PrivateTopic.new(id: 17, title: 'Hello', user: @user, last_user: @user, users: [@user])
29
33
  @private_topic = Thredded::PrivateTopicView.from_user(private_topic, @user)
30
- @private_posts = Thredded::TopicPostsPageView.new(@user, private_topic, private_topic.posts.page(1).limit(3))
34
+ @private_posts = Thredded::TopicPostsPageView.new(@user, private_topic, private_topic.posts
35
+ .send(Kaminari.config.page_method_name, 1)
36
+ .limit(3))
31
37
  @private_post = private_topic.posts.build(
32
38
  id: 1337, postable: private_topic, content: 'A private hello world', user: @user
33
39
  )
@@ -23,7 +23,7 @@ module Thredded
23
23
  page_scope = policy_scope(messageboard.topics)
24
24
  .order_sticky_first.order_recently_posted_first
25
25
  .includes(:categories, :last_user, :user)
26
- .page(current_page)
26
+ .send(Kaminari.config.page_method_name, current_page)
27
27
  return redirect_to(last_page_params(page_scope)) if page_beyond_last?(page_scope)
28
28
  @topics = Thredded::TopicsPageView.new(thredded_current_user, page_scope)
29
29
  @new_topic = init_new_topic
@@ -34,7 +34,7 @@ module Thredded
34
34
  .unread(thredded_current_user)
35
35
  .order_followed_first(thredded_current_user).order_recently_posted_first
36
36
  .includes(:categories, :last_user, :user)
37
- .page(current_page)
37
+ .send(Kaminari.config.page_method_name, current_page)
38
38
  return redirect_to(last_page_params(page_scope)) if page_beyond_last?(page_scope)
39
39
  @topics = Thredded::TopicsPageView.new(thredded_current_user, page_scope)
40
40
  @new_topic = init_new_topic
@@ -46,7 +46,7 @@ module Thredded
46
46
  .search_query(@query)
47
47
  .order_recently_posted_first
48
48
  .includes(:categories, :last_user, :user)
49
- .page(current_page)
49
+ .send(Kaminari.config.page_method_name, current_page)
50
50
  return redirect_to(last_page_params(page_scope)) if page_beyond_last?(page_scope)
51
51
  @topics = Thredded::TopicsPageView.new(thredded_current_user, page_scope)
52
52
  end
@@ -57,7 +57,7 @@ module Thredded
57
57
  page_scope = policy_scope(topic.posts)
58
58
  .order_oldest_first
59
59
  .includes(:user, :messageboard)
60
- .page(current_page)
60
+ .send(Kaminari.config.page_method_name, current_page)
61
61
  return redirect_to(last_page_params(page_scope)) if page_beyond_last?(page_scope)
62
62
  @posts = Thredded::TopicPostsPageView.new(thredded_current_user, topic, page_scope)
63
63
  Thredded::UserTopicReadState.touch!(thredded_current_user.id, page_scope.last) if thredded_signed_in?
@@ -79,7 +79,7 @@ module Thredded
79
79
  policy_scope(@category.topics)
80
80
  .unstuck
81
81
  .order_recently_posted_first
82
- .page(current_page)
82
+ .send(Kaminari.config.page_method_name, current_page)
83
83
  )
84
84
  render :index
85
85
  end
@@ -88,7 +88,7 @@ module Thredded
88
88
  @new_topic = Thredded::TopicForm.new(new_topic_params)
89
89
  authorize_creating @new_topic.topic
90
90
  if @new_topic.save
91
- redirect_to messageboard_topics_path(messageboard)
91
+ redirect_to next_page_after_create(params[:next_page])
92
92
  else
93
93
  render :new
94
94
  end
@@ -141,6 +141,19 @@ module Thredded
141
141
 
142
142
  private
143
143
 
144
+ def next_page_after_create(next_page)
145
+ case next_page
146
+ when 'messageboard', '', nil
147
+ return messageboard_topics_path(messageboard)
148
+ when 'topic'
149
+ messageboard_topic_path(messageboard, @new_topic.topic)
150
+ when %r{\A/[^/]\S+\z}
151
+ next_page
152
+ else
153
+ fail "Unexpected value for next page: #{next_page.inspect}"
154
+ end
155
+ end
156
+
144
157
  def in_messageboard?
145
158
  params.key?(:messageboard_id)
146
159
  end
@@ -5,6 +5,7 @@ module Thredded
5
5
  include ::Thredded::UrlsHelper
6
6
  include ::Thredded::NavHelper
7
7
  include ::Thredded::RenderHelper
8
+ include ::Thredded::IconHelper
8
9
 
9
10
  # @return [AllViewHooks] View hooks configuration.
10
11
  def view_hooks
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thredded
4
+ module IconHelper
5
+ def shared_inline_svg(filename, **args)
6
+ svg = content_tag :svg, **args do
7
+ content_tag :use, '', 'xlink:href' => "##{thredded_icon_id(filename)}"
8
+ end
9
+ if (definition = define_svg_icons(filename))
10
+ definition + svg
11
+ else
12
+ svg
13
+ end
14
+ end
15
+
16
+ def define_svg_icons(*filenames)
17
+ return if filenames.blank?
18
+ sb = filenames.map do |filename|
19
+ inline_svg_once(filename, id: thredded_icon_id(filename))
20
+ end
21
+ return if sb.compact.blank?
22
+ content_tag :div, safe_join(sb), class: 'thredded--svg-definitions'
23
+ end
24
+
25
+ def inline_svg_once(filename, id:, **transform_params)
26
+ return if @already_inlined_svg_ids&.include?(id)
27
+ record_already_inlined_svg(filename, id)
28
+ inline_svg(filename, id: id, **transform_params)
29
+ end
30
+
31
+ private
32
+
33
+ def record_already_inlined_svg(filename, id)
34
+ if filename.is_a?(String) # in case it's an IO or other
35
+ fail "Please use id: #{thredded_icon_id(filename)}" unless id == thredded_icon_id(filename)
36
+ end
37
+ @already_inlined_svg_ids ||= []
38
+ @already_inlined_svg_ids << id
39
+ end
40
+
41
+ def thredded_icon_id(svg_filename)
42
+ "thredded-#{File.basename(svg_filename, '.svg').dasherize}-icon"
43
+ end
44
+ end
45
+ end
@@ -35,7 +35,7 @@ module Thredded
35
35
 
36
36
  # Content that the user can moderate.
37
37
  if moderatable_messageboards != Thredded::Messageboard.none
38
- result = result.or(messageboard_id: moderatable_messageboards)
38
+ result = result.or(where(messageboard_id: moderatable_messageboards))
39
39
  end
40
40
  end
41
41
  result
@@ -69,17 +69,12 @@ module Thredded
69
69
  Thredded::TopicCommon::CachingHash.from_relation(read_states)
70
70
  end
71
71
 
72
- public
73
-
74
- # @param user [Thredded.user_class]
75
- # @return [Array<[TopicCommon, UserTopicReadStateCommon]>]
76
- def with_read_states(user)
77
- null_read_state = Thredded::NullUserTopicReadState.new
78
- return current_scope.zip([null_read_state]) if user.thredded_anonymous?
79
- read_states_by_postable = read_states_by_postable_hash(user)
80
- current_scope.map do |postable|
81
- [postable, read_states_by_postable[postable] || null_read_state]
82
- end
72
+ # @param [Thredded.user_class] user
73
+ # @param [Array<Number>] topic_ids
74
+ # @return [Hash{topic ID => posts count}] Counts of posts visible to the given user in the given topics.
75
+ def post_counts_for_user_and_topics(user, topic_ids)
76
+ return {} if topic_ids.empty?
77
+ Pundit.policy_scope!(user, post_class.all).where(postable_id: topic_ids).group(:postable_id).count
83
78
  end
84
79
  end
85
80
 
@@ -21,6 +21,11 @@ module Thredded
21
21
  post.created_at <= read_at
22
22
  end
23
23
 
24
+ # @return [Number]
25
+ def posts_count
26
+ read_posts_count + unread_posts_count
27
+ end
28
+
24
29
  def calculate_post_counts
25
30
  unread_posts_count, read_posts_count =
26
31
  self.class.visible_posts_scope(user)
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Thredded
4
+ # A Thredded::NullUser stands in place of a real (mainapp supplied) User when:
5
+ #
6
+ # * user is not logged in (ie. the thredded_current_user)
7
+ # * a user that was hard-deleted
8
+ # (e.g. if a post was made by a user, and then that user is destroyed, the post's user ID is nullified).
4
9
  class NullUser
5
10
  include ::Thredded::UserPermissions::Read::All
6
11
  include ::Thredded::UserPermissions::Write::None
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Thredded
4
4
  class NullUserTopicReadState
5
+ attr_reader :posts_count
6
+
7
+ def initialize(posts_count:)
8
+ @posts_count = posts_count
9
+ end
10
+
5
11
  def page
6
12
  1
7
13
  end
@@ -109,6 +109,24 @@ module Thredded
109
109
  def post_class
110
110
  Thredded::PrivatePost
111
111
  end
112
+
113
+ # @param [Thredded.user_class] user
114
+ # @return [Array<[PrivateTopic, PrivateUserTopicReadState]>]
115
+ def with_read_states(user)
116
+ if user.thredded_anonymous?
117
+ current_scope.map do |topic|
118
+ [topic, Thredded::NullUserTopicReadState.new(posts_count: topic.posts_count)]
119
+ end
120
+ else
121
+ read_states_by_postable = read_states_by_postable_hash(user)
122
+ current_scope.map do |topic|
123
+ [
124
+ topic,
125
+ read_states_by_postable[topic] || Thredded::NullUserTopicReadState.new(posts_count: topic.posts_count)
126
+ ]
127
+ end
128
+ end
129
+ end
112
130
  end
113
131
  end
114
132
  end
@@ -122,12 +122,26 @@ module Thredded
122
122
  # @param user [Thredded.user_class]
123
123
  # @return [Array<[TopicCommon, UserTopicReadStateCommon, UserTopicFollow]>]
124
124
  def with_read_and_follow_states(user)
125
- null_read_state = Thredded::NullUserTopicReadState.new
126
- return current_scope.zip([null_read_state, nil]) if user.thredded_anonymous?
127
- read_states_by_topic = read_states_by_postable_hash(user)
128
- follows_by_topic = follows_by_topic_hash(user)
129
- current_scope.map do |topic|
130
- [topic, read_states_by_topic[topic] || null_read_state, follows_by_topic[topic]]
125
+ topics = current_scope.to_a
126
+ if user.thredded_anonymous?
127
+ post_counts = post_counts_for_user_and_topics(user, topics.map(&:id))
128
+ topics.map do |topic|
129
+ [topic, Thredded::NullUserTopicReadState.new(posts_count: post_counts[topic.id] || 0), nil]
130
+ end
131
+ else
132
+ read_states_by_topic = read_states_by_postable_hash(user)
133
+ post_counts = post_counts_for_user_and_topics(
134
+ user, topics.reject { |topic| read_states_by_topic.key?(topic) }.map(&:id)
135
+ )
136
+ follows_by_topic = follows_by_topic_hash(user)
137
+ current_scope.map do |topic|
138
+ [
139
+ topic,
140
+ read_states_by_topic[topic] ||
141
+ Thredded::NullUserTopicReadState.new(posts_count: post_counts[topic.id] || 0),
142
+ follows_by_topic[topic]
143
+ ]
144
+ end
131
145
  end
132
146
  end
133
147
  end
@@ -4,7 +4,6 @@ module Thredded
4
4
  # A view model for TopicCommon.
5
5
  class BaseTopicView
6
6
  delegate :title,
7
- :posts_count,
8
7
  :last_post_at,
9
8
  :created_at,
10
9
  :user,
@@ -12,16 +11,16 @@ module Thredded
12
11
  :to_model,
13
12
  to: :@topic
14
13
 
15
- delegate :read?, :post_read?,
14
+ delegate :read?, :post_read?, :posts_count,
16
15
  to: :@read_state
17
16
 
18
- # @param topic [TopicCommon]
19
- # @param read_state [UserTopicReadStateCommon, nil]
20
- # @param policy [#destroy?]
17
+ # @param [TopicCommon] topic
18
+ # @param [UserTopicReadStateCommon, NullUserTopicReadState, nil] read_state
19
+ # @param [#destroy?] policy
21
20
  def initialize(topic, read_state, policy)
22
- @read_state = read_state || Thredded::NullUserTopicReadState.new
23
- @topic = topic
24
- @policy = policy
21
+ @topic = topic
22
+ @read_state = read_state || Thredded::NullUserTopicReadState.new(posts_count: @topic.posts_count)
23
+ @policy = policy
25
24
  end
26
25
 
27
26
  def states
@@ -10,22 +10,29 @@ module Thredded
10
10
  # @param [Thredded.user_class] user The user viewing the messageboards.
11
11
  # @param [Boolean] with_unread_topics_counts
12
12
  # @return [Array<MessageboardGroupView>]
13
- def self.grouped(messageboards_scope, user: nil, with_unread_topics_counts: user && !user.thredded_anonymous?)
13
+ def self.grouped( # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity
14
+ messageboards_scope, user: Thredded::NullUser.new, with_unread_topics_counts: !user.thredded_anonymous?
15
+ )
14
16
  scope = messageboards_scope.preload(last_topic: [:last_user])
15
17
  .eager_load(:group)
16
18
  .order(Arel.sql('COALESCE(thredded_messageboard_groups.position, 0) ASC, thredded_messageboard_groups.id ASC'))
17
19
  .ordered
20
+ topics_scope = Thredded::TopicPolicy::Scope.new(user, Thredded::Topic.all).resolve
18
21
  if with_unread_topics_counts
19
- topics_scope = Pundit.policy_scope!(user, Thredded::Topic)
20
22
  unread_topics_counts = messageboards_scope.unread_topics_counts(user: user, topics_scope: topics_scope)
21
23
  unread_followed_topics_counts = messageboards_scope.unread_topics_counts(
22
24
  user: user, topics_scope: topics_scope.followed_by(user)
23
25
  )
24
26
  end
27
+ topic_counts = topics_scope.group(:messageboard_id).count
28
+ posts_scope = Thredded::PostPolicy::Scope.new(user, Thredded::Post.all).resolve
29
+ post_counts = posts_scope.group(:messageboard_id).count
25
30
  scope.group_by(&:group).map do |(group, messageboards)|
26
31
  MessageboardGroupView.new(group, messageboards.map do |messageboard|
27
32
  MessageboardView.new(
28
33
  messageboard,
34
+ topics_count: topic_counts[messageboard.id] || 0,
35
+ posts_count: post_counts[messageboard.id] || 0,
29
36
  unread_topics_count: with_unread_topics_counts && unread_topics_counts[messageboard.id] || 0,
30
37
  unread_followed_topics_count:
31
38
  with_unread_topics_counts && unread_followed_topics_counts[messageboard.id] || 0
@@ -6,12 +6,16 @@ module Thredded
6
6
  delegate :name,
7
7
  :description,
8
8
  :locked?,
9
- :topics_count,
10
- :posts_count,
11
9
  :last_topic,
12
10
  :last_user,
13
11
  to: :@messageboard
14
12
 
13
+ # @return [Integer]
14
+ attr_reader :topics_count
15
+
16
+ # @return [Integer]
17
+ attr_reader :posts_count
18
+
15
19
  # @return [Integer]
16
20
  attr_reader :unread_topics_count
17
21
 
@@ -19,10 +23,20 @@ module Thredded
19
23
  attr_reader :unread_followed_topics_count
20
24
 
21
25
  # @param [Thredded::Messageboard] messageboard
26
+ # @param [Integer] topics_count
27
+ # @param [Integer] posts_count
22
28
  # @param [Integer] unread_topics_count
23
29
  # @param [Integer] unread_followed_topics_count
24
- def initialize(messageboard, unread_topics_count: 0, unread_followed_topics_count: 0)
30
+ def initialize(
31
+ messageboard,
32
+ topics_count: messageboard.topics_count,
33
+ posts_count: messageboard.posts_count,
34
+ unread_topics_count: 0,
35
+ unread_followed_topics_count: 0
36
+ )
25
37
  @messageboard = messageboard
38
+ @topics_count = topics_count
39
+ @posts_count = posts_count
26
40
  @unread_topics_count = unread_topics_count
27
41
  @unread_followed_topics_count = unread_followed_topics_count
28
42
  end
@@ -7,9 +7,9 @@ module Thredded
7
7
  :last_post, :messageboard_id, :messageboard_name,
8
8
  to: :@topic
9
9
 
10
- # @param topic [TopicCommon]
11
- # @param read_state [UserTopicReadStateCommon, nil]
12
- # @param policy [#destroy?]
10
+ # @param [Topic] topic
11
+ # @param [UserTopicReadState, NullUserTopicReadState, nil] read_state
12
+ # @param [#destroy?] policy
13
13
  def initialize(topic, read_state, follow, policy)
14
14
  super(topic, read_state, policy)
15
15
  @follow = follow
@@ -34,7 +34,7 @@ module Thredded
34
34
 
35
35
  # @return [Boolean] whether the topic is followed by the current user.
36
36
  def followed?
37
- @follow
37
+ !!@follow # rubocop:disable Style/DoubleNegation
38
38
  end
39
39
 
40
40
  def follow_reason
@@ -0,0 +1,2 @@
1
+ <%# Ensures that cells in incomplete rows are sized correctly (up to 5 missing cells). %>
2
+ <i class="thredded--grid-sizer"></i><i class="thredded--grid-sizer"></i><i class="thredded--grid-sizer"></i>
@@ -2,10 +2,7 @@
2
2
  <% content_for :thredded_page_id, 'thredded--messageboards-index' %>
3
3
  <% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
4
4
  <%= thredded_page do %>
5
- <div class="thredded--svg-definitions">
6
- <%= inline_svg 'thredded/follow.svg', id: 'thredded-follow-icon', title: nil %>
7
- <%= inline_svg 'thredded/lock.svg', id: 'thredded-lock-icon', title: nil %>
8
- </div>
5
+ <%= define_svg_icons('thredded/follow.svg', 'thredded/lock.svg') %>
9
6
  <%= view_hooks.messageboards_index.container.render self, groups: @groups do %>
10
7
  <section class="thredded--main-section thredded--messageboards">
11
8
  <%= view_hooks.messageboards_index.list.render self, groups: @groups do %>
@@ -18,6 +15,7 @@
18
15
  <%= render partial: 'thredded/messageboards/messageboard',
19
16
  collection: group.messageboards %>
20
17
  <% end %>
18
+ <%= render partial: 'thredded/messageboards/grid_sizers' %>
21
19
  </div>
22
20
  <% end %>
23
21
  <% end %>
@@ -2,7 +2,7 @@
2
2
  <% if messageboard.locked? %>
3
3
  <span class="thredded--messageboard--meta--locked"
4
4
  title="<%= t('thredded.messageboard.form.locked_notice') %>">
5
- <svg class="thredded--messageboard--meta--icon" role="img"><use xlink:href="#thredded-lock-icon"></use></svg>
5
+ <%= shared_inline_svg "thredded/lock.svg", class:"thredded--messageboard--meta--icon", role: "img"%>
6
6
  </span>
7
7
  <% end %>
8
8
  <h3 class="thredded--messageboard--meta--counts">
@@ -1,6 +1,6 @@
1
1
  <% if messageboard.unread_followed_topics? %>
2
2
  <span class="thredded--messageboard--unread-followed-topics-count">
3
- <svg class="thredded--messageboard--unread-followed-icon" role="img"><use xlink:href="#thredded-follow-icon"></use></svg>
3
+ <%= shared_inline_svg "thredded/follow.svg", class: "thredded--messageboard--unread-followed-icon", role: "img"%>
4
4
  <%= number_with_delimiter messageboard.unread_followed_topics_count %>
5
5
  </span>
6
6
  <% end %>
@@ -4,8 +4,9 @@
4
4
 
5
5
  <%= thredded_page do %>
6
6
  <%= content_tag :section,
7
- class: 'thredded--main-section thredded--private-topics',
8
- 'data-thredded-topics' => true do %>
7
+ class: 'thredded--main-section thredded--private-topics',
8
+ 'data-thredded-topics' => 'private',
9
+ 'data-thredded-topic-posts-per-page' => Thredded.posts_per_page do %>
9
10
 
10
11
  <% if @private_topics.empty? -%>
11
12
  <%= render 'thredded/private_topics/no_private_topics' %>
@@ -2,10 +2,11 @@
2
2
  <% if unread_topics_count > 0 || current %>
3
3
  <li class="thredded--user-navigation--unread-topics thredded--user-navigation--item<%= ' thredded--is-current' if current %>">
4
4
  <%= link_to current ? nav_back_path(messageboard) : unread_topics_path(messageboard: messageboard), rel: 'nofollow' do %>
5
- <%= inline_svg 'thredded/follow.svg', class: 'thredded--icon' %>
6
5
  <span class="thredded--nav-text"><%= t('thredded.nav.unread_topics') %></span>
7
6
  <% if unread_followed_topics_count > 0 -%>
8
- <span class="thredded--user-navigation--unread-topics--followed-count"><%= unread_followed_topics_count %></span>
7
+ <%= define_svg_icons 'thredded/follow.svg' %>
8
+ <span class="thredded--user-navigation--unread-topics--followed-count"><%=shared_inline_svg "thredded/follow.svg", class: "thredded--unread-topics--followed-icon", role:"img" %>
9
+ <span data-unread-followed-count><%= unread_followed_topics_count %></span></span>
9
10
  <% end -%>
10
11
  <% end %>
11
12
  </li>
@@ -9,10 +9,7 @@
9
9
  <%= thredded_page do %>
10
10
  <%= render 'section_title', label: 'messageboards#index', href: messageboards_path %>
11
11
  <%= content_tag :section, class: 'thredded--thredded--main-section thredded--messageboards' do %>
12
- <div class="thredded--svg-definitions">
13
- <%= inline_svg 'thredded/follow.svg', id: 'thredded-follow-icon', title: nil %>
14
- <%= inline_svg 'thredded/lock.svg', id: 'thredded-lock-icon', title: nil %>
15
- </div>
12
+ <%= define_svg_icons('thredded/follow.svg', 'thredded/lock.svg') %>
16
13
  <div class="thredded--messageboards-group">
17
14
  <%= render partial: 'thredded/messageboards/messageboard', collection: @messageboard_views %>
18
15
  </div>
@@ -7,6 +7,8 @@
7
7
  'data-autocomplete-min-length' => Thredded.autocomplete_min_length,
8
8
  'data-thredded-submit-hotkey' => true,
9
9
  } do |form| %>
10
+ <%= hidden_field_tag("next_page", params[:next_page]) %>
11
+
10
12
  <ul class="thredded--form-list on-top">
11
13
  <li class="title">
12
14
  <%= form.label :title, t('thredded.topics.form.title_label') %>
@@ -1,17 +1,20 @@
1
1
  <%= content_tag :article,
2
2
  id: dom_id(topic),
3
3
  class: ['thredded--topics--topic', topic_css_classes(topic)],
4
- data: {topic: topic.id, messageboard: topic.messageboard_id} do %>
4
+ data: {
5
+ topic: topic.id,
6
+ messageboard: topic.messageboard_id,
7
+ unread: !topic.read? || nil,
8
+ followed: topic.followed? || nil
9
+ } do %>
5
10
  <div class="thredded--topics--posts-count"><%= topic.posts_count %></div>
6
11
 
7
12
  <div class="thredded--topics--follow-info" title="<%= topic_follow_reason_text topic.follow_reason %>">
8
- <svg class="thredded--topics--follow-icon" role="img">
9
- <% if topic.followed? %>
10
- <use xlink:href="#thredded-follow-icon"></use>
11
- <% else %>
12
- <use xlink:href="#thredded-unfollow-icon"></use>
13
- <% end %>
14
- </svg>
13
+ <% if topic.followed? %>
14
+ <%= shared_inline_svg "thredded/follow.svg", class: "thredded--topics--follow-icon", role: "img" %>
15
+ <% else %>
16
+ <%= shared_inline_svg "thredded/unfollow.svg", class: "thredded--topics--follow-icon", role: "img" %>
17
+ <% end %>
15
18
  </div>
16
19
 
17
20
  <h1 class="thredded--topics--title">
@@ -3,12 +3,12 @@
3
3
  <% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
4
4
 
5
5
  <%= thredded_page do %>
6
- <div class="thredded--svg-definitions">
7
- <%= inline_svg 'thredded/follow.svg', id: 'thredded-follow-icon', title: nil %>
8
- <%= inline_svg 'thredded/unfollow.svg', id: 'thredded-unfollow-icon' %>
9
- </div>
6
+ <%= define_svg_icons('thredded/follow.svg', 'thredded/unfollow.svg') %>
10
7
 
11
- <%= content_tag :section, class: 'thredded--main-section thredded--topics', 'data-thredded-topics' => true do %>
8
+ <%= content_tag :section,
9
+ class: 'thredded--main-section thredded--topics',
10
+ 'data-thredded-topics' => true,
11
+ 'data-thredded-topic-posts-per-page' => Thredded.posts_per_page do %>
12
12
  <%= render 'thredded/topics/form',
13
13
  topic: @new_topic,
14
14
  css_class: 'thredded--is-compact',
@@ -16,8 +16,8 @@
16
16
  <%= render partial: 'thredded/topics/topic',
17
17
  collection: @topics,
18
18
  locals: {
19
- sticky_topics_divider: true,
20
- topics: @topics
19
+ sticky_topics_divider: true,
20
+ topics: @topics
21
21
  } %>
22
22
  <% end %>
23
23
 
@@ -7,11 +7,11 @@
7
7
 
8
8
  <%= thredded_page do %>
9
9
  <% if @topics.present? %>
10
- <div class="thredded--svg-definitions">
11
- <%= inline_svg 'thredded/follow.svg', id: 'thredded-follow-icon', title: nil %>
12
- <%= inline_svg 'thredded/unfollow.svg', id: 'thredded-unfollow-icon' %>
13
- </div>
14
- <%= content_tag :section, class: 'thredded--main-section thredded--topics', 'data-thredded-topics' => true do %>
10
+ <%= define_svg_icons('thredded/follow.svg', 'thredded/unfollow.svg') %>
11
+ <%= content_tag :section,
12
+ class: 'thredded--main-section thredded--topics',
13
+ 'data-thredded-topics' => true,
14
+ 'data-thredded-topic-posts-per-page' => Thredded.posts_per_page do %>
15
15
  <%= render 'thredded/topics/form',
16
16
  topic: @new_topic,
17
17
  css_class: 'thredded--is-compact',
@@ -85,7 +85,7 @@ class CreateThredded < Thredded::BaseMigration
85
85
 
86
86
  create_table :thredded_private_topics do |t|
87
87
  t.references :user, type: user_id_type, index: false
88
- t.references :last_user, index: false
88
+ t.references :last_user, type: user_id_type, index: false
89
89
  t.text :title, null: false
90
90
  t.text :slug, null: false
91
91
  t.integer :posts_count, default: 0
@@ -117,7 +117,7 @@ class CreateThredded < Thredded::BaseMigration
117
117
 
118
118
  create_table :thredded_topics do |t|
119
119
  t.references :user, type: user_id_type, index: false
120
- t.references :last_user, index: false
120
+ t.references :last_user, type: user_id_type, index: false
121
121
  t.text :title, null: false
122
122
  t.text :slug, null: false
123
123
  t.references :messageboard, null: false, index: false
@@ -128,9 +128,15 @@ Thredded.layout = 'thredded/application'
128
128
  #
129
129
  # Rails.application.config.to_prepare do
130
130
  # Thredded::ApplicationController.module_eval do
131
+ # # Render sign in page:
131
132
  # rescue_from Thredded::Errors::LoginRequired do |exception|
132
- # @message = exception.message
133
- # render template: 'sessions/new', status: :forbidden
133
+ # flash.now[:notice] = exception.message
134
+ # controller = Users::SessionsController.new
135
+ # controller.request = request
136
+ # controller.request.env['devise.mapping'] = Devise.mappings[:user]
137
+ # controller.response = response
138
+ # controller.response_options = { status: :forbidden }
139
+ # controller.process(:new)
134
140
  # end
135
141
  # end
136
142
  # end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Thredded
4
- VERSION = '0.16.0'
4
+ VERSION = '0.16.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thredded
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Oliveira
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-10-03 00:00:00.000000000 Z
12
+ date: 2018-10-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: active_record_union
@@ -31,14 +31,14 @@ dependencies:
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: 0.2.0
34
+ version: 0.3.0
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: 0.2.0
41
+ version: 0.3.0
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: friendly_id
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -792,6 +792,7 @@ files:
792
792
  - app/forms/thredded/topic_form.rb
793
793
  - app/forms/thredded/user_preferences_form.rb
794
794
  - app/helpers/thredded/application_helper.rb
795
+ - app/helpers/thredded/icon_helper.rb
795
796
  - app/helpers/thredded/nav_helper.rb
796
797
  - app/helpers/thredded/render_helper.rb
797
798
  - app/helpers/thredded/urls_helper.rb
@@ -878,6 +879,7 @@ files:
878
879
  - app/views/thredded/kaminari/_prev_page.html.erb
879
880
  - app/views/thredded/messageboard_groups/new.html.erb
880
881
  - app/views/thredded/messageboards/_form.html.erb
882
+ - app/views/thredded/messageboards/_grid_sizers.html.erb
881
883
  - app/views/thredded/messageboards/_messageboard.html.erb
882
884
  - app/views/thredded/messageboards/_messageboard_actions.html.erb
883
885
  - app/views/thredded/messageboards/edit.html.erb