decidim-posts 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +40 -0
  4. data/Rakefile +9 -0
  5. data/app/cells/decidim/posts/comments/add_comment.erb +15 -0
  6. data/app/cells/decidim/posts/comments/comments_loading.erb +1 -0
  7. data/app/cells/decidim/posts/comments/order_control.erb +13 -0
  8. data/app/cells/decidim/posts/comments/show.erb +31 -0
  9. data/app/cells/decidim/posts/comments_cell.rb +158 -0
  10. data/app/cells/decidim/posts/content_blocks/posts/show.erb +3 -0
  11. data/app/cells/decidim/posts/content_blocks/posts_cell.rb +50 -0
  12. data/app/cells/decidim/posts/content_blocks/posts_settings_form/show.erb +3 -0
  13. data/app/cells/decidim/posts/content_blocks/posts_settings_form_cell.rb +23 -0
  14. data/app/cells/decidim/posts/feed_dropdown_metadata_cell.rb +19 -0
  15. data/app/cells/decidim/posts/meeting/show.erb +65 -0
  16. data/app/cells/decidim/posts/meeting_cell.rb +44 -0
  17. data/app/cells/decidim/posts/post/show.erb +30 -0
  18. data/app/cells/decidim/posts/post/survey.erb +68 -0
  19. data/app/cells/decidim/posts/post_attachments/show.erb +40 -0
  20. data/app/cells/decidim/posts/post_attachments_cell.rb +17 -0
  21. data/app/cells/decidim/posts/post_cell.rb +44 -0
  22. data/app/cells/decidim/posts/post_comment/show.erb +17 -0
  23. data/app/cells/decidim/posts/post_comment_cell.rb +49 -0
  24. data/app/cells/decidim/posts/post_header/show.erb +53 -0
  25. data/app/cells/decidim/posts/post_header_cell.rb +79 -0
  26. data/app/cells/decidim/posts/post_host/show.erb +74 -0
  27. data/app/cells/decidim/posts/post_host_cell.rb +73 -0
  28. data/app/cells/decidim/posts/post_metadata/show.erb +9 -0
  29. data/app/cells/decidim/posts/post_metadata_cell.rb +67 -0
  30. data/app/cells/decidim/posts/reaction_menu/show.erb +18 -0
  31. data/app/cells/decidim/posts/reaction_menu/styles.erb +7 -0
  32. data/app/cells/decidim/posts/reaction_menu_cell.rb +26 -0
  33. data/app/cells/decidim/posts/reactions/show.erb +8 -0
  34. data/app/cells/decidim/posts/reactions_cell.rb +22 -0
  35. data/app/commands/decidim/posts/add_reaction_to_resource.rb +90 -0
  36. data/app/commands/decidim/posts/create_post.rb +112 -0
  37. data/app/commands/decidim/posts/destroy_post.rb +20 -0
  38. data/app/commands/decidim/posts/remove_reaction_from_resource.rb +41 -0
  39. data/app/commands/decidim/posts/update_post.rb +72 -0
  40. data/app/controllers/concerns/decidim/posts/admin/filterable.rb +46 -0
  41. data/app/controllers/concerns/decidim/posts/reactionable.rb +31 -0
  42. data/app/controllers/decidim/posts/admin/application_controller.rb +26 -0
  43. data/app/controllers/decidim/posts/admin/posts_controller.rb +93 -0
  44. data/app/controllers/decidim/posts/application_controller.rb +18 -0
  45. data/app/controllers/decidim/posts/meetings_controller.rb +176 -0
  46. data/app/controllers/decidim/posts/posts_controller.rb +202 -0
  47. data/app/controllers/decidim/posts/reactions_controller.rb +54 -0
  48. data/app/controllers/decidim/posts/user_answers_controller.rb +36 -0
  49. data/app/events/decidim/posts/resource_reacted_event.rb +38 -0
  50. data/app/forms/decidim/posts/answer_form.rb +13 -0
  51. data/app/forms/decidim/posts/post_form.rb +121 -0
  52. data/app/forms/decidim/posts/question_form.rb +20 -0
  53. data/app/helpers/decidim/posts/admin/posts_helper.rb +8 -0
  54. data/app/helpers/decidim/posts/application_helper.rb +20 -0
  55. data/app/helpers/decidim/posts/post_cells_helper.rb +35 -0
  56. data/app/helpers/decidim/posts/posts_helper.rb +34 -0
  57. data/app/helpers/decidim/posts/reaction_helper.rb +22 -0
  58. data/app/models/decidim/posts/answer.rb +13 -0
  59. data/app/models/decidim/posts/application_record.rb +10 -0
  60. data/app/models/decidim/posts/post.rb +116 -0
  61. data/app/models/decidim/posts/question.rb +17 -0
  62. data/app/models/decidim/posts/reaction.rb +22 -0
  63. data/app/models/decidim/posts/reaction_type.rb +13 -0
  64. data/app/models/decidim/posts/user_answer.rb +11 -0
  65. data/app/packs/entrypoints/decidim_posts.js +5 -0
  66. data/app/packs/entrypoints/decidim_posts.scss +1 -0
  67. data/app/packs/images/decidim/posts/icon.svg +1 -0
  68. data/app/packs/src/decidim/posts/carousel.js +112 -0
  69. data/app/packs/src/decidim/posts/host_status.js +75 -0
  70. data/app/packs/src/decidim/posts/newFeeds.js +98 -0
  71. data/app/packs/src/decidim/posts/posts.js +272 -0
  72. data/app/packs/src/decidim/posts/submenu.js +46 -0
  73. data/app/packs/src/decidim/posts/survey.js +94 -0
  74. data/app/packs/stylesheets/decidim/posts/_variables.scss +10 -0
  75. data/app/packs/stylesheets/decidim/posts/posts.scss +415 -0
  76. data/app/permissions/decidim/posts/admin/permissions.rb +23 -0
  77. data/app/permissions/decidim/posts/permissions.rb +101 -0
  78. data/app/presenters/decidim/posts/post_presenter.rb +45 -0
  79. data/app/views/decidim/posts/admin/posts/_post-tr.html.erb +45 -0
  80. data/app/views/decidim/posts/admin/posts/index.html.erb +54 -0
  81. data/app/views/decidim/posts/meetings/edit.html.erb +24 -0
  82. data/app/views/decidim/posts/posts/_admin_options.html.erb +30 -0
  83. data/app/views/decidim/posts/posts/_attachment.html.erb +24 -0
  84. data/app/views/decidim/posts/posts/_edit_form.html.erb +16 -0
  85. data/app/views/decidim/posts/posts/_feed.html.erb +8 -0
  86. data/app/views/decidim/posts/posts/_form.html.erb +104 -0
  87. data/app/views/decidim/posts/posts/_index.html.erb +49 -0
  88. data/app/views/decidim/posts/posts/_meeting_form.erb +18 -0
  89. data/app/views/decidim/posts/posts/_new.html.erb +33 -0
  90. data/app/views/decidim/posts/posts/_new_survey.html.erb +20 -0
  91. data/app/views/decidim/posts/posts/_new_survey_answer.html.erb +11 -0
  92. data/app/views/decidim/posts/posts/_new_survey_question.html.erb +17 -0
  93. data/app/views/decidim/posts/posts/_post.html.erb +11 -0
  94. data/app/views/decidim/posts/posts/_post_form.html.erb +5 -0
  95. data/app/views/decidim/posts/posts/_sidebar.html.erb +36 -0
  96. data/app/views/decidim/posts/posts/edit.html.erb +34 -0
  97. data/app/views/decidim/posts/posts/index.html.erb +1 -0
  98. data/app/views/decidim/posts/posts/show.html.erb +12 -0
  99. data/app/views/decidim/posts/reactions/_update_buttons_and_counters.html.erb +1 -0
  100. data/config/assets.rb +9 -0
  101. data/config/i18n-tasks.yml +10 -0
  102. data/config/locales/bs.yml +215 -0
  103. data/config/locales/de.yml +217 -0
  104. data/config/locales/en.yml +216 -0
  105. data/config/locales/hr.yml +219 -0
  106. data/config/locales/it.yml +215 -0
  107. data/config/locales/sr.yml +220 -0
  108. data/config/locales/tr.yml +219 -0
  109. data/lib/decidim/posts/admin.rb +10 -0
  110. data/lib/decidim/posts/admin_engine.rb +25 -0
  111. data/lib/decidim/posts/component.rb +59 -0
  112. data/lib/decidim/posts/content_blocks/content_blocks_homepage.rb +19 -0
  113. data/lib/decidim/posts/engine.rb +84 -0
  114. data/lib/decidim/posts/test/factories.rb +14 -0
  115. data/lib/decidim/posts/version.rb +9 -0
  116. data/lib/decidim/posts.rb +13 -0
  117. metadata +183 -0
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Posts
5
+ class PostCell < Decidim::ViewModel
6
+ include PostCellsHelper
7
+ include Cell::ViewModel::Partial
8
+
9
+ def show
10
+ render :show
11
+ end
12
+
13
+ def post
14
+ model
15
+ end
16
+
17
+ def post_body
18
+ simple_format(post_presenter.body)
19
+ # translated_attribute model.body
20
+ end
21
+
22
+ def post_presenter
23
+ @post_presenter ||= present(model)
24
+ end
25
+
26
+ def post_category
27
+ model.category
28
+ end
29
+
30
+ def post_commentable?
31
+ model.enable_comments?
32
+ end
33
+
34
+ def resource_image_path
35
+ model.attachments.first&.url
36
+ end
37
+
38
+ def has_image?
39
+ resource_image_path.present?
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ <% if post_commentable? %>
2
+ <div class="posts__post_comments" data-post-id="<%= post.id %>">
3
+ <button class="button button__sm button__transparent-primary absolute newCommentBtn" data-post-id="<%= post.id %>" data-model-type="<%= model_type %>" aria-expanded="false" aria-controls="new_comment_for_<%= model_type %>_<%= post.id %> comments-for-<%= model_type %>-<%= post.id %>-threads"><%= t(".comment") %></button>
4
+ <%#= comments_for post %>
5
+ <%= cell(
6
+ "decidim/posts/comments",
7
+ post,
8
+ machine_translations: machine_translations_toggled?,
9
+ single_comment: params.fetch("commentId", nil),
10
+ order: options[:order],
11
+ polymorphic: options[:polymorphic]
12
+ ).to_s %>
13
+ </div>
14
+ <% end %>
15
+ <style>
16
+ .comment__opinion-label, .comment__opinion-container{display:none;}
17
+ </style>
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Posts
5
+ class PostCommentCell < Decidim::ViewModel
6
+ include Cell::ViewModel::Partial
7
+ include Decidim::ResourceHelper
8
+ include Decidim::Comments::CommentsHelper
9
+ def show
10
+ render :show
11
+ end
12
+
13
+ def post
14
+ model
15
+ end
16
+
17
+ def model_type
18
+ if model.is_a?(Decidim::Posts::Post)
19
+ "Post"
20
+ elsif model.is_a?(Decidim::Meetings::Meeting)
21
+ "Meeting"
22
+ else
23
+ "Post"
24
+ end
25
+ end
26
+
27
+ def post_commentable?
28
+ if model.respond_to?(:enable_comments?)
29
+ model.enable_comments?
30
+ elsif model.respond_to?(:comments_enabled?)
31
+ model.comments_enabled?
32
+ else
33
+ false # Default to false if neither method exists
34
+ end
35
+ end
36
+
37
+ def machine_translations_toggled?
38
+ RequestStore.store[:toggle_machine_translations]
39
+ end
40
+
41
+ def comments_count
42
+ model.comments_count
43
+ end
44
+
45
+
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,53 @@
1
+ <div class="card__grid-metadata">
2
+ <% if post.is_a?(Decidim::Posts::Post) && post.highlighted %>
3
+ <div></div>
4
+ <% else %>
5
+ <% if post.author.is_a? Decidim::User %>
6
+ <%= cell("decidim/author", Decidim::UserPresenter.new(post.author), layout: :compact) %>
7
+ <% else %>
8
+ <%= cell("decidim/author", Decidim::Meetings::OfficialAuthorPresenter.new, layout: :compact) %>
9
+ <% end %>
10
+ <% end %>
11
+ <div class="posts__post_actions">
12
+ <div class="posts__post_actions_submenu relative self-start">
13
+ <% if show_menu? %>
14
+ <button type="button" aria-expanded="false" aria-haspopup="true" aria-controls="<%= dom_id(post) %>_posts__post_actions_submenu" id="<%= dom_id(post) %>_posts__post_actions_submenuButton" class="flex justify-center items-start min-w-[24px] min-h-[24px] openButton">
15
+ <span class="sr-only"><%= t('decidim.components.posts.actions.openMenu') %></span>
16
+ <span aria-hidden="true">...</span>
17
+ </button>
18
+
19
+ <ul id="<%= dom_id(post) %>_posts__post_actions_submenu" role="menu" aria-orientation="vertical" aria-labelledby="<%= dom_id(post) %>_posts__post_actions_submenuButton" class="absolute right-0 mt-2 px-4 py-2 bg-white divide-y divide-dashed divide-gray-100 rounded-md shadow-feedMenu overflow-hidden hidden z-10">
20
+ <% if show_report_button? %>
21
+ <li class="text-right py-1 text-feeds-sidebar" role="menuitem">
22
+ <%= cell("decidim/report_button", post, button_classes: "hover:text-black dropdown__item flex-row-reverse min-h-[24px]", modal_id: "flagModalPost#{post.id}") %>
23
+ </li>
24
+ <% end %>
25
+ <% if show_translate_button? %>
26
+ <li class="text-right py-1 text-feeds-sidebar" role="menuitem">
27
+ <%= link_to '#', class:'hover:text-black dropdown__item min-h-[24px]' do %>
28
+ <%= icon "translate" %>
29
+ <span><%= t('decidim.components.posts.actions.translate') %></span>
30
+ <% end %>
31
+ </li>
32
+ <% end %>
33
+ <% if post_editable? %>
34
+ <li class="text-right py-1 text-feeds-sidebar" role="menuitem">
35
+ <%= link_to edit_post_path, class: "hover:text-black dropdown__item min-h-[24px]" do %>
36
+ <%= icon "pencil-line" %>
37
+ <span><%= t('decidim.components.posts.actions.edit') %></span>
38
+ <% end %>
39
+ </li>
40
+ <% end %>
41
+ <% if post_deleteable? %>
42
+ <li class="text-right py-1 text-feeds-sidebar" role="menuitem">
43
+ <%= button_to delete_post_path, method: delete_method, class: "hover:text-black dropdown__item min-h-[24px]", data: { confirm: t('decidim.components.posts.actions.deleteConfirmation') } do %>
44
+ <%= icon "delete-bin-line" %>
45
+ <span><%= t('decidim.components.posts.actions.delete') %></span>
46
+ <% end %>
47
+ </li>
48
+ <% end %>
49
+ </ul>
50
+ <% end %>
51
+ </div>
52
+ </div>
53
+ </div>
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Posts
5
+ class PostHeaderCell < Decidim::ViewModel
6
+ include Cell::ViewModel::Partial
7
+ include Decidim::EndorsableHelper
8
+ def show
9
+ render :show
10
+ end
11
+
12
+ def post
13
+ model
14
+ end
15
+
16
+ def edit_post_path
17
+ if post.component.manifest_name == "meetings"
18
+ # use MeetingsController from posts module
19
+ Decidim::EngineRouter.main_proxy(posts_component_for_meeting(post)).edit_meeting_path(post)
20
+ else
21
+ Decidim::EngineRouter.main_proxy(post.component).edit_post_path(post)
22
+ end
23
+ end
24
+
25
+ def delete_post_path
26
+ if post.component.manifest_name == "meetings"
27
+ # use MeetingsController from posts module
28
+ Decidim::EngineRouter.main_proxy(posts_component_for_meeting(post)).withdraw_meeting_path(assembly_slug: post.component.participatory_space.slug, component_id: 1, id: post)
29
+ else
30
+ Decidim::EngineRouter.main_proxy(post.component).post_path(post)
31
+ end
32
+ end
33
+
34
+ def delete_method
35
+ if post.component.manifest_name == "meetings"
36
+ :put
37
+ else
38
+ :delete
39
+ end
40
+ end
41
+
42
+ def post_editable?
43
+ if post.component.manifest_name == "meetings"
44
+ post.authored_by?(current_user)
45
+ else
46
+ # Don't allow editing of survey posts
47
+ return false if post.category == "survey"
48
+ post.editable_by?(current_user)
49
+ end
50
+ end
51
+
52
+ def post_deleteable?
53
+ if post.component.manifest_name == "meetings"
54
+ post.withdrawable_by?(current_user)
55
+ else
56
+ post.deleteable_by?(current_user)
57
+ end
58
+ end
59
+
60
+ def show_report_button?
61
+ post.author != current_user && post.class == Decidim::Posts::Post
62
+ end
63
+
64
+ def show_translate_button?
65
+ false # disbled for now
66
+ end
67
+
68
+ def show_menu?
69
+ show_report_button? || post_editable? || post_deleteable? || show_translate_button?
70
+ end
71
+
72
+ private
73
+
74
+ def posts_component_for_meeting(meeting)
75
+ @posts_component ||= meeting.component.participatory_space.components.find_by(manifest_name: "posts")
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,74 @@
1
+ <%= cell("decidim/posts/post_header", post) %>
2
+
3
+ <% unless post_highlighted %>
4
+ <% if official_post %>
5
+ <%= icon "home-gear-line", class: 'host-icon' %>
6
+ <% end %>
7
+ <% end %>
8
+
9
+ <%= cell("decidim/posts/post_attachments", post) %>
10
+ <div class="card__grid-text">
11
+ <% should_truncate = strip_tags(post_body).length > 300 %>
12
+ <div class="content-block__description editor-content" <%= "data-component='accordion'" if should_truncate %>>
13
+ <% if should_truncate %>
14
+ <% seed = SecureRandom.hex(4) %>
15
+ <div id="panel-view-more-<%= seed %>" aria-hidden="true">
16
+ <%= post_body %>
17
+ </div>
18
+ <button class="button button__sm button__text-secondary mt-2" data-controls="panel-view-more-<%= seed %>" aria-expanded="false">
19
+ <span>
20
+ <%= t("view_more", scope: "layouts.decidim.announcements") %>
21
+ </span>
22
+ <%= icon "arrow-down-s-line" %>
23
+ <span>
24
+ <%= t("view_less", scope: "layouts.decidim.announcements") %>
25
+ </span>
26
+ <%= icon "arrow-up-s-line" %>
27
+ </button>
28
+ <% else %>
29
+ <%= post_body %>
30
+ <% end %>
31
+ </div>
32
+ </div>
33
+ <% if status_class %>
34
+ <div class="bg-white rounded-2xl border-4 border-<%= status_class %> flex-col justify-between items-center inline-flex mx-2">
35
+ <div class="self-stretch px-2 py-1 justify-start items-center gap-2 inline-flex">
36
+ <time class="p-2 font-bold text-lg"><%= I18n.l(post.updated_at, format: :long) %></time>
37
+ </div>
38
+ <div class="self-stretch grow shrink basis-0 bg-<%= status_class %> rounded-lg justify-center items-center gap-2 inline-flex">
39
+ <div class="p-2 text-xl font-bold uppercase text-black"><%= post_status_text %></div>
40
+ </div>
41
+ </div>
42
+ <% end %>
43
+
44
+ <% unless post_highlighted || status_class %>
45
+ <% if post_commentable? %>
46
+ <%= cell("decidim/posts/post_comment", post) %>
47
+ <% end %>
48
+ <% end %>
49
+ <% if participatory_space.admins.exists?(id:current_user.id) && !official_post %>
50
+ <div class="flex flex-col flex-wrap py-4 px-2 justify-center items-center gap-4">
51
+ <% if post_status && post_status != 0 %>
52
+ <% modalDialog = t('decidim.posts.posts.host.status.dialogBodyMsg') %>
53
+ <% else %>
54
+ <% modalDialog = t('decidim.posts.posts.host.status.dialogBodyMsg') + "<br>" + t('decidim.posts.posts.host.status.dialogBodyMsgComments') %>
55
+ <% end %>
56
+ <% if post_status && post_status != 0 %>
57
+ <button class="button button__sm button__transparent-primary changeStatusButton" data-post-status="0" data-post-id="<%= post.id %>" data-dialogmsg="<%=modalDialog%>" aria-controls="confirmationModal" aria-haspopup="dialog">
58
+ <%= t('decidim.posts.posts.host.status.undone') %>
59
+ <%= icon('close-line') %>
60
+ </button>
61
+ <% end %>
62
+ <button class="button button__sm button__transparent-primary changeStatusButton" data-post-status="1" data-post-id="<%= post.id %>" data-dialogmsg="<%=modalDialog%>" aria-controls="confirmationModal" aria-haspopup="dialog">
63
+ <%= t('decidim.posts.posts.host.status.processing') %>
64
+ <%= icon('arrow-right-line') %>
65
+ </button>
66
+ <button class="button button__sm button__transparent-primary changeStatusButton" data-post-status="2" data-post-id="<%= post.id %>" data-dialogmsg="<%=modalDialog%>" aria-controls="confirmationModal" aria-haspopup="dialog">
67
+ <%= t('decidim.posts.posts.host.status.done') %>
68
+ <%= icon('arrow-right-line') %>
69
+ </button>
70
+ </div>
71
+ <% end %>
72
+ <% unless post_highlighted %>
73
+ <%= cell("decidim/posts/post_metadata", post) %>
74
+ <% end %>
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Posts
5
+ class PostHostCell < Decidim::ViewModel
6
+ include Cell::ViewModel::Partial
7
+ include ActiveModel::Conversion
8
+ include PostCellsHelper
9
+
10
+ delegate :allowed_to?, to: :controller, prefix: false
11
+
12
+ def show
13
+ render :show
14
+ end
15
+
16
+ def post
17
+ model
18
+ end
19
+
20
+ def post_body
21
+ translated_attribute model.body
22
+ end
23
+
24
+ def post_category
25
+ model.category
26
+ end
27
+
28
+ # post status / for host post
29
+ # 0 default
30
+ # 1 bearbeitung
31
+ # 2 erledigt
32
+
33
+ def post_status
34
+ model.status
35
+ end
36
+
37
+ def status_class
38
+ case post_status
39
+ when 0
40
+ nil
41
+ when 1
42
+ 'warning'
43
+ when 2
44
+ 'success'
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ def post_status_text
51
+ case post_status
52
+ when 1
53
+ I18n.t('decidim.posts.posts.host.status.processing')
54
+ when 2
55
+ I18n.t('decidim.posts.posts.host.status.done')
56
+ end
57
+ end
58
+
59
+ def post_highlighted
60
+ model.highlighted
61
+ end
62
+
63
+ def post_commentable?
64
+ model.enable_comments?
65
+ end
66
+
67
+ def official_post
68
+ participatory_space.admins.exists?(id: post.author.id)
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,9 @@
1
+ <div class="posts__post_metadata px-2 py-1 text-sm border-t-[1px] border-feeds-<%=category%> flex justify-between items-end ">
2
+ <div class="flex flex-col gap-1">
3
+ <div id="feeds_post-<%= post.id %>_reactions" class="flex gap-2 items-center" aria-live="polite">
4
+ <%= cell("decidim/posts/reactions", post) if post.is_a?(Decidim::Posts::Post) %>
5
+ </div>
6
+ <%= cell("decidim/posts/reaction_menu", post) if post.is_a?(Decidim::Posts::Post) %>
7
+ </div>
8
+ <time><%= @time_since_update %></time>
9
+ </div>
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Posts
5
+ class PostMetadataCell < Decidim::ViewModel
6
+ include Cell::ViewModel::Partial
7
+ include Decidim::EndorsableHelper
8
+ def show
9
+ @time_since_update = time_since_update
10
+ @category_info = category_info
11
+ render :show
12
+ end
13
+
14
+ def post
15
+ model
16
+ end
17
+
18
+ private
19
+
20
+ def time_since_update
21
+ time_difference = Time.now - post.created_at
22
+ minutes = (time_difference / 60).to_i
23
+ hours = (time_difference / 3600).to_i
24
+ days = (time_difference / 86400).to_i
25
+ months = (time_difference / 2592000).to_i # Approximate month duration
26
+ years = (time_difference / 31536000).to_i
27
+
28
+ if minutes < 60
29
+ minutes.to_s + ' ' + t("decidim.posts.time.minutes", count: minutes)
30
+ elsif hours < 24
31
+ hours.to_s + ' ' + t("decidim.posts.time.hours", count: hours)
32
+ elsif days < 30
33
+ days.to_s + ' ' + t("decidim.posts.time.days", count: days)
34
+ elsif months < 12
35
+ months.to_s + ' ' + t("decidim.posts.time.months", count: months)
36
+ else
37
+ years.to_s + ' ' + t("decidim.posts.time.years", count: years)
38
+ end
39
+ end
40
+
41
+ def category_info
42
+ filters = [
43
+ { filter: 'post', icon: 'home-5-line' },
44
+ { filter: 'sharecare', icon: 'shake-hands-line' },
45
+ { filter: 'calendar', icon: 'calendar-line' },
46
+ { filter: 'survey', icon: 'draft-line' },
47
+ { filter: 'host', icon: 'home-gear-line' }
48
+ ]
49
+
50
+ current_filter = filters.find { |f| f[:filter] == category }
51
+ {
52
+ icon: current_filter[:icon],
53
+ label: t("decidim.components.posts.filter.#{current_filter[:filter]}")
54
+ }
55
+ end
56
+
57
+ def category
58
+ if post.is_a?(Decidim::Meetings::Meeting)
59
+ 'calendar'
60
+ else
61
+ post.category
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,18 @@
1
+ <div class="relative posts__post_reactions_submenu">
2
+ <% user_reaction = post.reactions.find_by(author: current_user) %>
3
+ <% data_icon = user_reaction ? "data-reaction=\"#{user_reaction.reaction_type.name}\"" : 'data-reaction=""' %>
4
+ <button type="button" aria-expanded="false" aria-haspopup="true" aria-controls="<%= dom_id(post) %>_posts__post_reactions_submenu" id="<%= dom_id(post) %>_posts__post_reactions_submenuButton" class="openButton flex gap-1 items-center text-xl" <%=data_icon%> aria-live="polite">
5
+ <span class="text-sm"><%= t("decidim.endorsement_buttons_cell.already_endorsed") %></span>
6
+ <span class="reaction_icon text-black"><%= icon('heart-add-line', class: 'text-black') %></span>
7
+ </button>
8
+ <ul id="<%= dom_id(post) %>_posts__post_reactions_submenu" role="menu" aria-orientation="vertical" aria-labelledby="<%= dom_id(post) %>_posts__post_reactions_submenuButton" class="absolute left-[60px] -top-[40px] p-2 flex gap-2 bg-white rounded-md shadow-feedMenu overflow-hidden hidden" aria-label="<%= t("decidim.components.posts.reactions.title") %>">
9
+ <% reaction_types.each do |reaction_type| %>
10
+ <li role="menuitem">
11
+ <a href="#" class="reaction_btn block text-xl w-[24px] h-[24px]" data-reaction="<%= reaction_type.name %>" data-reaction-id="<%= reaction_type.id %>" data-reaction-url="<%= reactions_path %>" data-reactionable-id="<%= post.to_global_id.to_s %>">
12
+ <%= reaction_icon(reaction_type.icon_name) %>
13
+ <span class="sr-only"><%= t("decidim.components.posts.reactions.#{reaction_type.name}") %></span>
14
+ </a>
15
+ </li>
16
+ <% end %>
17
+ </ul>
18
+ </div>
@@ -0,0 +1,7 @@
1
+ <style>
2
+ <% reaction_types.each do |reaction_type| %>
3
+ .posts__post_reactions_submenu button[data-reaction=<%= reaction_type.name %>]::after{
4
+ content: '<%= reaction_icon(reaction_type.icon_name) %>';
5
+ }
6
+ <% end %>
7
+ </style>
@@ -0,0 +1,26 @@
1
+ module Decidim
2
+ module Posts
3
+ class ReactionMenuCell < Decidim::ViewModel
4
+ include PostCellsHelper
5
+ include ReactionHelper
6
+ include Cell::ViewModel::Partial
7
+
8
+ def show
9
+ render :show
10
+ end
11
+
12
+ def styles
13
+ render :styles
14
+ end
15
+
16
+ def post
17
+ model
18
+ end
19
+
20
+ def reaction_types
21
+ ReactionType.order(:position)
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ <div class="flex pl-1">
2
+ <% grouped_reactions_for(post).each do |reaction_type, reactions| %>
3
+ <span class="text-sm ml-[-0.25rem]"><%= reaction_icon(reaction_type.icon_name) %></span>
4
+ <% end %>
5
+ <span class="total-reactions ml-1">
6
+ <%= post.reactions_count if post.reactions_count > 0 %>
7
+ </span>
8
+ </div>
@@ -0,0 +1,22 @@
1
+ module Decidim
2
+ module Posts
3
+ class ReactionsCell < Decidim::ViewModel
4
+ include PostCellsHelper
5
+ include ReactionHelper
6
+ include Cell::ViewModel::Partial
7
+
8
+ def show
9
+ render :show
10
+ end
11
+
12
+ def post
13
+ model
14
+ end
15
+
16
+ def grouped_reactions_for(resource)
17
+ resource.reactions.group_by(&:reaction_type)
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Posts
5
+ # A command with all the business logic related with a user reacting to a resource.
6
+ class AddReactionToResource < Decidim::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # resource - An instance of Decidim::Posts::Reactionable.
10
+ # current_user - The current user.
11
+ # current_group_id- (optional) The current_grup that is reacting to the Resource.
12
+ # reaction_type_id - The id of the reaction type (emoji) that the user is reacting with.
13
+ def initialize(resource, current_user, current_group_id = nil, reaction_type_id)
14
+ @resource = resource
15
+ @current_user = current_user
16
+ @current_group_id = current_group_id
17
+ @reaction_type = ReactionType.find(reaction_type_id)
18
+ end
19
+
20
+ # Executes the command. Broadcasts these events:
21
+ #
22
+ # - :ok when everything is valid, together with the resource reaction.
23
+ # - :invalid if the form was not valid and we could not proceed.
24
+ #
25
+ # Returns nothing.
26
+ def call
27
+ return broadcast(:invalid) if existing_group_reaction?
28
+
29
+ reaction = @resource.reactions.find_by(author: @current_user)
30
+ if reaction.present?
31
+ if reaction.reaction_type == @reaction_type
32
+ if delete_reaction(reaction)
33
+ broadcast(:ok, nil)
34
+ else
35
+ broadcast(:invalid)
36
+ end
37
+ else
38
+ if reaction.update(reaction_type: @reaction_type)
39
+ broadcast(:ok, reaction)
40
+ else
41
+ broadcast(:invalid)
42
+ end
43
+ end
44
+ else
45
+ reaction = build_resource_reaction unless reaction
46
+ if reaction.save
47
+ notify_reactor_followers
48
+ broadcast(:ok, reaction)
49
+ else
50
+ broadcast(:invalid)
51
+ end
52
+ end
53
+ rescue ActiveRecord::RecordNotUnique
54
+ broadcast(:invalid)
55
+ end
56
+
57
+ private
58
+
59
+ def existing_group_reaction?
60
+ @current_group_id.present? && @resource.reactions.exists?(decidim_user_group_id: @current_group_id)
61
+ end
62
+
63
+ def build_resource_reaction
64
+ reaction = @resource.reactions.build(author: @current_user, reaction_type: @reaction_type)
65
+ reaction.user_group = user_groups.find(@current_group_id) if @current_group_id.present?
66
+ reaction
67
+ end
68
+
69
+ def delete_reaction(reaction)
70
+ reaction.destroy
71
+ end
72
+
73
+ def user_groups
74
+ Decidim::UserGroups::ManageableUserGroups.for(@current_user).verified
75
+ end
76
+
77
+ def notify_reactor_followers
78
+ Decidim::EventsManager.publish(
79
+ event: "decidim.events.resource_reacted",
80
+ event_class: Decidim::Posts::ResourceReactedEvent,
81
+ resource: @resource,
82
+ followers: @current_user.followers,
83
+ extra: {
84
+ reactor_id: @current_user.id
85
+ }
86
+ )
87
+ end
88
+ end
89
+ end
90
+ end