thredded 0.16.0 → 0.16.1

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 (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