thredded 0.10.0 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -17
  3. data/app/assets/images/thredded/three-dot-menu.svg +3 -0
  4. data/app/assets/stylesheets/thredded/base/_tables.scss +1 -0
  5. data/app/assets/stylesheets/thredded/base/_variables.scss +24 -1
  6. data/app/assets/stylesheets/thredded/components/_currently-online.scss +1 -0
  7. data/app/assets/stylesheets/thredded/components/_messageboard.scss +18 -10
  8. data/app/assets/stylesheets/thredded/components/_post.scss +84 -13
  9. data/app/assets/stylesheets/thredded/components/_topics.scss +7 -1
  10. data/app/assets/stylesheets/thredded/layout/_main-container.scss +1 -0
  11. data/app/assets/stylesheets/thredded/layout/_search-navigation.scss +15 -6
  12. data/app/controllers/thredded/application_controller.rb +6 -3
  13. data/app/controllers/thredded/moderation_controller.rb +1 -1
  14. data/app/controllers/thredded/posts_controller.rb +19 -22
  15. data/app/controllers/thredded/preferences_controller.rb +1 -2
  16. data/app/controllers/thredded/private_posts_controller.rb +77 -0
  17. data/app/controllers/thredded/private_topics_controller.rb +1 -1
  18. data/app/controllers/thredded/read_states_controller.rb +1 -1
  19. data/app/controllers/thredded/topics_controller.rb +1 -1
  20. data/app/forms/thredded/private_topic_form.rb +3 -3
  21. data/app/forms/thredded/topic_form.rb +1 -1
  22. data/app/helpers/thredded/application_helper.rb +12 -1
  23. data/app/helpers/thredded/render_helper.rb +14 -0
  24. data/app/helpers/thredded/urls_helper.rb +8 -0
  25. data/app/models/concerns/thredded/post_common.rb +20 -0
  26. data/app/models/concerns/thredded/user_topic_read_state_common.rb +6 -0
  27. data/app/models/thredded/null_user_topic_read_state.rb +4 -0
  28. data/app/policies/thredded/post_policy.rb +4 -0
  29. data/app/policies/thredded/private_post_policy.rb +4 -0
  30. data/app/view_hooks/thredded/all_view_hooks.rb +15 -0
  31. data/app/view_models/thredded/base_topic_view.rb +1 -1
  32. data/app/view_models/thredded/post_view.rb +23 -21
  33. data/app/view_models/thredded/posts_page_view.rb +4 -2
  34. data/app/view_models/thredded/topic_posts_page_view.rb +1 -1
  35. data/app/view_models/thredded/topic_view.rb +1 -1
  36. data/app/view_models/thredded/topics_page_view.rb +1 -0
  37. data/app/views/thredded/moderation/_post.html.erb +2 -2
  38. data/app/views/thredded/moderation/_user_post.html.erb +2 -2
  39. data/app/views/thredded/moderation/activity.html.erb +3 -1
  40. data/app/views/thredded/moderation/pending.html.erb +3 -1
  41. data/app/views/thredded/moderation/user.html.erb +3 -1
  42. data/app/views/thredded/posts/_content.html.erb +1 -0
  43. data/app/views/thredded/posts/_post.html.erb +11 -12
  44. data/app/views/thredded/posts/edit.html.erb +3 -4
  45. data/app/views/thredded/posts_common/_actions.html.erb +21 -8
  46. data/app/views/thredded/posts_common/actions/_delete.html.erb +4 -0
  47. data/app/views/thredded/posts_common/actions/_edit.html.erb +2 -0
  48. data/app/views/thredded/posts_common/actions/_mark_as_unread.html.erb +2 -0
  49. data/app/views/thredded/private_posts/_content.html.erb +1 -0
  50. data/app/views/thredded/private_posts/_private_post.html.erb +5 -6
  51. data/app/views/thredded/private_posts/edit.html.erb +18 -0
  52. data/app/views/thredded/private_topics/show.html.erb +3 -1
  53. data/app/views/thredded/shared/_nav.html.erb +1 -1
  54. data/app/views/thredded/shared/nav/_standalone.html.erb +1 -1
  55. data/app/views/thredded/topics/_sticky_topics_divider.html.erb +1 -0
  56. data/app/views/thredded/topics/_topic.html.erb +4 -0
  57. data/app/views/thredded/topics/index.html.erb +1 -1
  58. data/app/views/thredded/topics/show.html.erb +1 -1
  59. data/app/views/thredded/users/_post.html.erb +2 -2
  60. data/app/views/thredded/users/_posts.html.erb +1 -1
  61. data/config/locales/en.yml +1 -0
  62. data/config/locales/es.yml +1 -0
  63. data/config/locales/pl.yml +1 -0
  64. data/config/locales/pt-BR.yml +1 -0
  65. data/config/routes.rb +9 -4
  66. data/lib/generators/thredded/install/templates/initializer.rb +7 -0
  67. data/lib/thredded.rb +4 -0
  68. data/lib/thredded/collection_to_strings_with_cache_renderer.rb +62 -0
  69. data/lib/thredded/version.rb +1 -1
  70. metadata +15 -4
@@ -1,4 +1,4 @@
1
- <%# @param post [Thredded::PostView] %>
1
+ <% post, content = post_and_content if local_assigns.key?(:post_and_content) %>
2
2
  <%= content_tag :article, id: dom_id(post), class: 'thredded--post thredded--post-moderation' do %>
3
3
  <%= render 'thredded/posts_common/header_with_user_and_topic',
4
4
  post: post,
@@ -8,7 +8,7 @@
8
8
  content_tag :em, t('thredded.null_user_name')
9
9
  end
10
10
  %>
11
- <%= render 'thredded/posts_common/content', post: post %>
11
+ <%= content || render('thredded/posts/content', post: post) %>
12
12
  <%= render 'thredded/posts_common/actions', post: post %>
13
13
  <% if post.blocked? %>
14
14
  <p class="thredded--alert thredded--alert-danger">
@@ -1,7 +1,7 @@
1
- <%# @param post [Thredded::PostView] %>
1
+ <% post, content = post_and_content if local_assigns.key?(:post_and_content) %>
2
2
  <%= content_tag :article, id: dom_id(post), class: 'thredded--post thredded--post-moderation' do %>
3
3
  <%= render 'thredded/posts_common/header_with_topic', post: post %>
4
- <%= render 'thredded/posts_common/content', post: post %>
4
+ <%= content || render('thredded/posts/content', post: post) %>
5
5
  <%= render 'thredded/posts_common/actions', post: post %>
6
6
  <% if post.blocked? %>
7
7
  <p class="thredded--alert thredded--alert-danger">
@@ -11,7 +11,9 @@
11
11
  </div>
12
12
  <% end %>
13
13
  <% if @posts.present? %>
14
- <%= render partial: 'post', collection: @posts %>
14
+ <%= render_posts @posts,
15
+ partial: 'thredded/moderation/post',
16
+ content_partial: 'thredded/posts/content' %>
15
17
  <%= paginate @posts %>
16
18
  <% end %>
17
19
  <% end %>
@@ -10,7 +10,9 @@
10
10
  </div>
11
11
  <% end %>
12
12
  <% if @posts.present? %>
13
- <%= render partial: 'post', collection: @posts %>
13
+ <%= render_posts @posts,
14
+ partial: 'thredded/moderation/post',
15
+ content_partial: 'thredded/posts/content' %>
14
16
  <%= paginate @posts %>
15
17
  <% else %>
16
18
  <div class="thredded--empty">
@@ -42,7 +42,9 @@
42
42
  <% end %>
43
43
  <% if @posts.present? %>
44
44
  <h2><%= t 'thredded.users.recent_activity' %></h2>
45
- <%= render partial: 'user_post', collection: @posts, as: :post %>
45
+ <%= render_posts @posts,
46
+ partial: 'thredded/moderation/user_post',
47
+ content_partial: 'thredded/posts/content' %>
46
48
  <%= paginate @posts %>
47
49
  <% end %>
48
50
  <% end %>
@@ -0,0 +1 @@
1
+ <%= render 'thredded/posts_common/content', post: post %>
@@ -1,14 +1,13 @@
1
- <% cache(post, expires_in: 1.week) do %>
2
- <%= content_tag :article, id: dom_id(post), class: 'thredded--post' do %>
3
- <%= render 'thredded/posts_common/header', post: post %>
4
- <%= render 'thredded/posts_common/content', post: post %>
5
- <%= render 'thredded/posts_common/actions', post: post %>
6
- <% if post.pending_moderation? && !Thredded.content_visible_while_pending_moderation %>
7
- <p class="thredded--alert thredded--alert-warning"><%= t 'thredded.posts.pending_moderation_notice' %></p>
8
- <% elsif post.blocked? && post.can_moderate? %>
9
- <p class="thredded--alert thredded--alert-danger">
10
- <%= render 'thredded/shared/content_moderation_blocked_state', moderation_record: post.last_moderation_record %>
11
- </p>
12
- <% end %>
1
+ <% post, content = post_and_content if local_assigns.key?(:post_and_content) %>
2
+ <%= content_tag :article, id: dom_id(post), class: "thredded--post thredded--#{post.read_state}--post" do %>
3
+ <%= render 'thredded/posts_common/actions', post: post %>
4
+ <%= render 'thredded/posts_common/header', post: post %>
5
+ <%= content || render('thredded/posts/content', post: post) %>
6
+ <% if post.pending_moderation? && !Thredded.content_visible_while_pending_moderation %>
7
+ <p class="thredded--alert thredded--alert-warning"><%= t 'thredded.posts.pending_moderation_notice' %></p>
8
+ <% elsif post.blocked? && post.can_moderate? %>
9
+ <p class="thredded--alert thredded--alert-danger">
10
+ <%= render 'thredded/shared/content_moderation_blocked_state', moderation_record: post.last_moderation_record %>
11
+ </p>
13
12
  <% end %>
14
13
  <% end %>
@@ -1,4 +1,4 @@
1
- <% content_for :thredded_page_title, 'Edit Post' %>
1
+ <% content_for :thredded_page_title, t('thredded.nav.edit_post') %>
2
2
  <% content_for :thredded_page_id, 'thredded--edit-post' %>
3
3
  <% content_for :thredded_breadcrumbs do %>
4
4
  <ul class="thredded--navigation-breadcrumbs">
@@ -9,11 +9,10 @@
9
9
  <%= thredded_page do %>
10
10
  <section class="thredded--main-section">
11
11
  <%= render 'thredded/posts/form',
12
- messageboard: (messageboard unless @post.private_topic_post?),
12
+ messageboard: messageboard,
13
13
  topic: topic,
14
14
  post: @post,
15
- preview_url: (@post.private_topic_post? ? private_topic_private_post_preview_path(@post.postable, @post)
16
- : messageboard_topic_post_preview_path(messageboard, @post.postable, @post)),
15
+ preview_url: messageboard_topic_post_preview_path(messageboard, @post.postable, @post),
17
16
  button_text: t('thredded.posts.form.update_btn'),
18
17
  button_submitting_text: t('thredded.posts.form.update_btn_submitting')%>
19
18
  </section>
@@ -1,11 +1,24 @@
1
- <% if post.can_update? %>
2
- <%= link_to t('thredded.posts.edit'), post.edit_path, class: 'thredded--post--edit' %>
1
+ <% actions = capture do %>
2
+ <%= view_hooks.post_common.actions.render self, post: post do %>
3
+ <% if post.can_update? %>
4
+ <%= render 'thredded/posts_common/actions/edit', post: post %>
5
+ <% end %>
6
+ <% if post.can_destroy? %>
7
+ <%= render 'thredded/posts_common/actions/delete', post: post %>
8
+ <% end %>
9
+ <% if post.read_state %>
10
+ <%= view_hooks.post_common.mark_as_unread.render self, post: post do %>
11
+ <%= render 'thredded/posts_common/actions/mark_as_unread', post: post %>
12
+ <% end %>
13
+ <% end %>
14
+ <% end %>
3
15
  <% end %>
4
16
 
5
- <% if post.can_destroy? %>
6
- <%= link_to t('thredded.posts.delete'), post.destroy_path,
7
- method: :delete,
8
- class: 'thredded--post--delete',
9
- data: { confirm: I18n.t('thredded.posts.delete_confirm') }
10
- %>
17
+ <%- if actions.present? %>
18
+ <div class='thredded--post--dropdown'>
19
+ <%= inline_svg 'thredded/three-dot-menu.svg', class: 'thredded--post--dropdown--toggle' %>
20
+ <div class='thredded--post--dropdown--actions'>
21
+ <%= actions %>
22
+ </div>
23
+ </div>
11
24
  <% end %>
@@ -0,0 +1,4 @@
1
+ <%= button_to t('thredded.posts.delete'), post.destroy_path,
2
+ method: :delete,
3
+ class: 'thredded--post--delete thredded--post--dropdown--actions--item',
4
+ data: {confirm: I18n.t('thredded.posts.delete_confirm')} %>
@@ -0,0 +1,2 @@
1
+ <%= link_to t('thredded.posts.edit'), post.edit_path,
2
+ class: 'thredded--post--edit thredded--post--dropdown--actions--item' %>
@@ -0,0 +1,2 @@
1
+ <%= button_to(t('thredded.topics.mark_as_unread'), post.mark_unread_path, method: :post, class:
2
+ 'thredded--post--mark-as-unread thredded--post--dropdown--actions--item') %>
@@ -0,0 +1 @@
1
+ <%= render 'thredded/posts_common/content', post: post %>
@@ -1,7 +1,6 @@
1
- <% cache(private_post, expires_in: 1.week) do %>
2
- <%= content_tag :article, id: dom_id(private_post), class: 'thredded--post' do %>
3
- <%= render 'thredded/posts_common/header', post: private_post %>
4
- <%= render 'thredded/posts_common/content', post: private_post %>
5
- <%= render 'thredded/posts_common/actions', post: private_post %>
6
- <% end %>
1
+ <% private_post, content = post_and_content if local_assigns.key?(:post_and_content) %>
2
+ <%= content_tag :article, id: dom_id(private_post), class: 'thredded--post' do %>
3
+ <%= render 'thredded/posts_common/actions', post: private_post %>
4
+ <%= render 'thredded/posts_common/header', post: private_post %>
5
+ <%= content || render('thredded/private_posts/content', post: post) %>
7
6
  <% end %>
@@ -0,0 +1,18 @@
1
+ <% content_for :thredded_page_title, t('thredded.nav.edit_post') %>
2
+ <% content_for :thredded_page_id, 'thredded--edit-post' %>
3
+ <% content_for :thredded_breadcrumbs do %>
4
+ <ul class="thredded--navigation-breadcrumbs">
5
+ <li><%= link_to t('thredded.nav.edit_post'), edit_post_path(@post) %></li>
6
+ </ul>
7
+ <% end %>
8
+
9
+ <%= thredded_page do %>
10
+ <section class="thredded--main-section">
11
+ <%= render 'thredded/posts/form',
12
+ topic: topic,
13
+ post: @post,
14
+ preview_url: private_topic_private_post_preview_path(@post.postable, @post),
15
+ button_text: t('thredded.posts.form.update_btn'),
16
+ button_submitting_text: t('thredded.posts.form.update_btn_submitting')%>
17
+ </section>
18
+ <% end %>
@@ -11,7 +11,9 @@
11
11
  <%= view_hooks.posts_common.pagination_top.render(self, posts: @posts) do %>
12
12
  <footer class="thredded--pagination-top"><%= paginate @posts %></footer>
13
13
  <% end %>
14
- <%= render partial: 'thredded/private_posts/private_post', collection: @posts, cached: true %>
14
+ <%= render_posts @posts,
15
+ partial: 'thredded/private_posts/private_post',
16
+ content_partial: 'thredded/private_posts/content' %>
15
17
  <%= view_hooks.posts_common.pagination_bottom.render(self, posts: @posts) do %>
16
18
  <footer class="thredded--pagination-bottom"><%= paginate @posts %></footer>
17
19
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <nav class="thredded--navigation">
2
2
  <ul class="thredded--user-navigation<%= ' thredded--user-navigation-standalone' if Thredded.standalone_layout? %>">
3
- <% if signed_in? && Thredded.standalone_layout? %>
3
+ <% if thredded_signed_in? && Thredded.standalone_layout? %>
4
4
  <li class="thredded--user-navigation--profile thredded--user-navigation--item">
5
5
  <%= link_to thredded_current_user.thredded_display_name, user_path(thredded_current_user) %>
6
6
  </li>
@@ -1,6 +1,6 @@
1
1
  <li class="thredded--user-navigation--standalone thredded--user-navigation--item">
2
2
  <% resource_name = Thredded.user_class.name.underscore %>
3
- <% if signed_in? %>
3
+ <% if thredded_signed_in? %>
4
4
  <%= link_to main_app.send(:"destroy_#{resource_name}_session_path"), method: :delete do %>
5
5
  <span>Sign Out</span>
6
6
  <% end %>
@@ -0,0 +1 @@
1
+ <hr class="thredded--topics--sticky-topics-divider">
@@ -40,3 +40,7 @@
40
40
  </span>
41
41
  <% end %>
42
42
  <% end %>
43
+
44
+ <% if !topic_iteration.last? && topic.sticky? && !topics[topic_counter + 1].sticky? %>
45
+ <%= render 'thredded/topics/sticky_topics_divider' %>
46
+ <% end %>
@@ -15,7 +15,7 @@
15
15
  css_class: 'thredded--is-compact',
16
16
  preview_url: preview_new_messageboard_topic_path(messageboard),
17
17
  placeholder: t('thredded.topics.form.title_placeholder_start') if @new_topic %>
18
- <%= render @topics %>
18
+ <%= render partial: 'thredded/topics/topic', collection: @topics, locals: {topics: @topics} %>
19
19
  <% end %>
20
20
 
21
21
  <footer class="thredded--pagination-bottom">
@@ -12,7 +12,7 @@
12
12
  <%= view_hooks.posts_common.pagination_top.render(self, posts: @posts) do %>
13
13
  <footer class="thredded--pagination-top"><%= paginate @posts %></footer>
14
14
  <% end %>
15
- <%= render partial: 'thredded/posts/post', collection: @posts, cached: true %>
15
+ <%= render_posts @posts, partial: 'thredded/posts/post', content_partial: 'thredded/posts/content' %>
16
16
  <%= view_hooks.posts_common.pagination_bottom.render(self, posts: @posts) do %>
17
17
  <footer class="thredded--pagination-bottom"><%= paginate @posts %></footer>
18
18
  <% end %>
@@ -1,6 +1,6 @@
1
- <%# @param post [Thredded::PostView] %>
1
+ <% post, content = post_and_content if local_assigns.key?(:post_and_content) %>
2
2
  <%= content_tag :article, id: dom_id(post), class: 'thredded--post' do %>
3
3
  <%= render 'thredded/posts_common/header_with_topic', post: post %>
4
- <%= render 'thredded/posts_common/content', post: post %>
4
+ <%= content || render('thredded/posts/content', post: post) %>
5
5
  <%= render 'thredded/posts_common/actions', post: post %>
6
6
  <% end %>
@@ -4,4 +4,4 @@
4
4
  TODO: Use a Cell instead. https://github.com/apotonick/cells
5
5
  %>
6
6
  <%# @param posts [Thredded::PostsPageView] %>
7
- <%= render partial: 'thredded/users/post', collection: posts %>
7
+ <%= render_posts posts, partial: 'thredded/users/post', content_partial: 'thredded/posts/content' %>
@@ -157,6 +157,7 @@ en:
157
157
  update_btn: Update Topic
158
158
  locked:
159
159
  label: Locked
160
+ mark_as_unread: Mark unread from here
160
161
  not_following: You are not following this topic.
161
162
  search:
162
163
  no_results_message: There are no results for your search - %{query}
@@ -157,6 +157,7 @@ es:
157
157
  update_btn: Actualizar Tema
158
158
  locked:
159
159
  label: Bloqueado
160
+ mark_as_unread: Marcar sin leer desde aquí
160
161
  not_following: No estás siguiendo este tema.
161
162
  search:
162
163
  no_results_message: No hay resultados para tu búsqueda - %{query}
@@ -156,6 +156,7 @@ pl:
156
156
  update_btn: Zaktualizuj temat
157
157
  locked:
158
158
  label: Zablokowany
159
+ mark_as_unread: Oznacz jako nieprzeczytane stąd
159
160
  not_following: Nie obserwujesz tego tematu.
160
161
  search:
161
162
  no_results_message: 'Nie odnaleziono żadnych wyników dla frazy: %{query}'
@@ -160,6 +160,7 @@ pt-BR:
160
160
  update_btn: Atualizar Tópico
161
161
  locked:
162
162
  label: Trancado
163
+ mark_as_unread: Marca não lida a partir daqui
163
164
  not_following: Você não está seguindo este tema.
164
165
  search:
165
166
  no_results_message: Nenhum resultado encontrado para sua busca - %{query}
data/config/routes.rb CHANGED
@@ -2,8 +2,7 @@
2
2
  Thredded::Engine.routes.draw do # rubocop:disable Metrics/BlockLength
3
3
  resource :theme_preview, only: [:show], path: 'theme-preview' if %w(development test).include? Rails.env
4
4
 
5
- positive_int = /[1-9]\d*/
6
- page_constraint = { page: positive_int }
5
+ page_constraint = { page: /[1-9]\d*/ }
7
6
 
8
7
  scope path: 'private-topics' do
9
8
  resource :read_state, only: [:update], as: :mark_all_private_topics_read
@@ -14,14 +13,17 @@ Thredded::Engine.routes.draw do # rubocop:disable Metrics/BlockLength
14
13
  member do
15
14
  get '(page-:page)', action: :show, as: '', constraints: page_constraint
16
15
  end
17
- resources :private_posts, path: '', except: [:index, :show], controller: 'posts' do
16
+ resources :private_posts, path: '', except: [:index, :show] do
18
17
  post :preview, on: :new, controller: 'private_post_previews'
19
18
  resource :preview, only: [:update], controller: 'private_post_previews'
19
+ member do
20
+ post 'mark_as_unread'
21
+ end
20
22
  end
21
23
  end
22
24
  end
23
25
 
24
- scope only: [:show], constraints: { id: positive_int } do
26
+ scope only: [:show], constraints: { id: Thredded.routes_id_constraint } do
25
27
  resources :private_post_permalinks, path: 'private-posts'
26
28
  resources :post_permalinks, path: 'posts'
27
29
  end
@@ -71,6 +73,9 @@ Thredded::Engine.routes.draw do # rubocop:disable Metrics/BlockLength
71
73
  resources :posts, except: [:index, :show], path: '' do
72
74
  post :preview, on: :new, controller: 'post_previews'
73
75
  resource :preview, only: [:update], controller: 'post_previews'
76
+ member do
77
+ post 'mark_as_unread'
78
+ end
74
79
  end
75
80
  end
76
81
  end
@@ -29,6 +29,13 @@ Thredded.current_user_method = :"current_#{Thredded.user_class.name.underscore}"
29
29
  # User avatar URL. rb-gravatar gem is used by default:
30
30
  Thredded.avatar_url = ->(user) { Gravatar.src(user.email, 128, 'mm') }
31
31
 
32
+ # ==> Database Configuration
33
+ # By default, thredded uses integers for record ID route constraints.
34
+ # For integer based IDs (default):
35
+ # Thredded.routes_id_constraint = /[1-9]\d*/
36
+ # For UUID based IDs (example):
37
+ # Thredded.routes_id_constraint = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
38
+
32
39
  # ==> Permissions Configuration
33
40
  # By default, thredded uses a simple permission model, where all the users can post to all message boards,
34
41
  # and admins and moderators are determined by a flag on the users table.
data/lib/thredded.rb CHANGED
@@ -39,6 +39,8 @@ require 'thredded/view_hooks/renderer'
39
39
  # Require Thredded::ContentFormatter explicitly so that it doesn't need to be required if used in the initializer.
40
40
  require 'thredded/content_formatter'
41
41
 
42
+ require 'thredded/collection_to_strings_with_cache_renderer'
43
+
42
44
  module Thredded
43
45
  mattr_accessor \
44
46
  :autocomplete_min_length,
@@ -48,6 +50,7 @@ module Thredded
48
50
  :email_outgoing_prefix,
49
51
  :layout,
50
52
  :messageboards_order,
53
+ :routes_id_constraint,
51
54
  :user_class,
52
55
  :user_display_name_method,
53
56
  :user_name_column,
@@ -92,6 +95,7 @@ module Thredded
92
95
  self.show_topic_followers = false
93
96
  self.messageboards_order = :position
94
97
  self.autocomplete_min_length = 2
98
+ self.routes_id_constraint = /[1-9]\d*/
95
99
 
96
100
  # @return [Thredded::AllViewHooks] View hooks configuration.
97
101
  def self.view_hooks
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ require 'action_view/renderer/abstract_renderer'
3
+ module Thredded
4
+ class CollectionToStringsWithCacheRenderer < ActionView::AbstractRenderer
5
+ # @param view_context
6
+ # @param collection [Array<T>]
7
+ # @param partial [String]
8
+ # @param expires_in [ActiveSupport::Duration]
9
+ # @return Array<[T, String]>
10
+ def render_collection_to_strings_with_cache( # rubocop:disable Metrics/ParameterLists
11
+ view_context, collection:, partial:, expires_in:, locals: {}, **opts
12
+ )
13
+ template = @lookup_context.find_template(partial, [], true, locals, {})
14
+ collection = collection.to_a
15
+ instrument(:collection, count: collection.size) do |instrumentation_payload|
16
+ return [] if collection.blank?
17
+ keyed_collection = collection.each_with_object({}) do |item, hash|
18
+ key = ActiveSupport::Cache.expand_cache_key(
19
+ view_context.cache_fragment_name(item, virtual_path: template.virtual_path), :views
20
+ )
21
+ # #read_multi & #write may require key mutability, Dalli 2.6.0.
22
+ hash[key.frozen? ? key.dup : key] = item
23
+ end
24
+ cache = collection_cache
25
+ cached_partials = cache.read_multi(*keyed_collection.keys)
26
+ instrumentation_payload[:cache_hits] = cached_partials.size if instrumentation_payload
27
+
28
+ collection_to_render = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
29
+ rendered_partials = render_partials(
30
+ view_context, collection: collection_to_render, partial: partial, locals: locals, **opts
31
+ ).each
32
+
33
+ keyed_collection.map do |cache_key, item|
34
+ [item, cached_partials[cache_key] || rendered_partials.next.tap do |rendered|
35
+ cache.write(cache_key, rendered, expires_in: expires_in)
36
+ end]
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def collection_cache
44
+ if ActionView::PartialRenderer.respond_to?(:collection_cache)
45
+ # Rails 5.0+
46
+ ActionView::PartialRenderer.collection_cache
47
+ else
48
+ # Rails 4.2.x
49
+ Rails.application.config.action_controller.cache_store
50
+ end
51
+ end
52
+
53
+ # @return [Array<String>]
54
+ def render_partials(view_context, collection:, **opts)
55
+ return [] if collection.empty?
56
+ partial_renderer = ActionView::PartialRenderer.new(@lookup_context)
57
+ collection.map do |item|
58
+ partial_renderer.render(view_context, opts.merge(object: item), nil)
59
+ end
60
+ end
61
+ end
62
+ end