decidim-posts 1.0.0

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