decidim-comments 0.24.2 → 0.25.0.rc3

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -20
  3. data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -1
  4. data/app/cells/decidim/comments/comment/actions.erb +1 -1
  5. data/app/cells/decidim/comments/comment/deletion_data.erb +1 -0
  6. data/app/cells/decidim/comments/comment/show.erb +30 -21
  7. data/app/cells/decidim/comments/comment/utilities.erb +40 -12
  8. data/app/cells/decidim/comments/comment/votes.erb +6 -6
  9. data/app/cells/decidim/comments/comment_cell.rb +29 -0
  10. data/app/cells/decidim/comments/comment_form/show.erb +1 -1
  11. data/app/cells/decidim/comments/comments/add_comment.erb +10 -6
  12. data/app/cells/decidim/comments/comments/order_control.erb +4 -5
  13. data/app/cells/decidim/comments/comments/show.erb +2 -4
  14. data/app/cells/decidim/comments/comments/user_comments_blocked_warning.erb +5 -1
  15. data/app/cells/decidim/comments/comments_cell.rb +24 -2
  16. data/app/cells/decidim/comments/edit_comment_modal_form/show.erb +29 -0
  17. data/app/cells/decidim/comments/edit_comment_modal_form_cell.rb +53 -0
  18. data/app/commands/decidim/comments/create_comment.rb +2 -1
  19. data/app/commands/decidim/comments/delete_comment.rb +46 -0
  20. data/app/commands/decidim/comments/update_comment.rb +62 -0
  21. data/app/controllers/decidim/comments/comments_controller.rb +63 -6
  22. data/app/events/decidim/comments/comment_voted_event.rb +9 -0
  23. data/app/forms/decidim/comments/comment_form.rb +1 -1
  24. data/app/models/decidim/comments/comment.rb +24 -1
  25. data/app/packs/src/decidim/comments/comments.component.js +300 -0
  26. data/app/{assets/javascripts → packs/src}/decidim/comments/comments.component.test.js +116 -26
  27. data/app/packs/src/decidim/comments/comments.component_for_testing.js +8 -0
  28. data/app/packs/src/decidim/comments/comments.js +1 -0
  29. data/app/permissions/decidim/comments/permissions.rb +10 -1
  30. data/app/queries/decidim/comments/metrics/comment_participants_metric_measure.rb +1 -1
  31. data/app/queries/decidim/comments/metrics/comments_metric_manage.rb +1 -1
  32. data/app/queries/decidim/comments/sorted_comments.rb +8 -6
  33. data/app/views/decidim/comments/comments/_delete.html.erb +5 -0
  34. data/app/views/decidim/comments/comments/_edited_comment.html.erb +1 -0
  35. data/app/views/decidim/comments/comments/create.js.erb +4 -2
  36. data/app/views/decidim/comments/comments/delete.js.erb +17 -0
  37. data/app/views/decidim/comments/comments/deletion_error.js.erb +1 -0
  38. data/app/views/decidim/comments/comments/reload.js.erb +2 -0
  39. data/app/views/decidim/comments/comments/update.js.erb +8 -0
  40. data/app/views/decidim/comments/comments/update_error.js.erb +1 -0
  41. data/config/assets.rb +5 -0
  42. data/config/locales/ar.yml +0 -1
  43. data/config/locales/ca.yml +7 -1
  44. data/config/locales/cs.yml +25 -1
  45. data/config/locales/de.yml +7 -1
  46. data/config/locales/el.yml +0 -1
  47. data/config/locales/en.yml +25 -1
  48. data/config/locales/es-MX.yml +7 -1
  49. data/config/locales/es-PY.yml +7 -1
  50. data/config/locales/es.yml +7 -1
  51. data/config/locales/fi-plain.yml +25 -1
  52. data/config/locales/fi.yml +25 -1
  53. data/config/locales/fr-CA.yml +25 -1
  54. data/config/locales/fr-LU.yml +162 -0
  55. data/config/locales/fr.yml +25 -1
  56. data/config/locales/gl.yml +25 -1
  57. data/config/locales/hu.yml +0 -1
  58. data/config/locales/it.yml +38 -1
  59. data/config/locales/ja.yml +35 -1
  60. data/config/locales/lb-LU.yml +1 -0
  61. data/config/locales/lv.yml +0 -1
  62. data/config/locales/nl.yml +27 -2
  63. data/config/locales/no.yml +0 -1
  64. data/config/locales/pl.yml +7 -1
  65. data/config/locales/pt-BR.yml +61 -0
  66. data/config/locales/pt.yml +0 -1
  67. data/config/locales/ro-RO.yml +25 -1
  68. data/config/locales/sk.yml +0 -1
  69. data/config/locales/sr-CS.yml +0 -1
  70. data/config/locales/sv.yml +25 -1
  71. data/config/locales/tr-TR.yml +0 -1
  72. data/config/locales/zh-CN.yml +0 -1
  73. data/db/migrate/20200706123136_make_comments_handle_i18n.rb +1 -1
  74. data/db/migrate/20210402124534_add_participatory_process_to_comments.rb +12 -0
  75. data/db/migrate/20210529095942_add_deleted_at_column_to_comments.rb +7 -0
  76. data/lib/decidim/comments/commentable.rb +6 -1
  77. data/lib/decidim/comments/commentable_with_component.rb +33 -0
  78. data/lib/decidim/comments/engine.rb +1 -9
  79. data/lib/decidim/comments/test/factories.rb +1 -0
  80. data/lib/decidim/comments/version.rb +1 -1
  81. data/lib/decidim/comments.rb +1 -0
  82. data/lib/tasks/decidim_comments.rake +15 -0
  83. metadata +32 -29
  84. data/app/assets/config/decidim_comments_manifest.js +0 -1
  85. data/app/assets/javascripts/decidim/comments/comments.component.js.es6 +0 -292
  86. data/app/assets/javascripts/decidim/comments/comments.js.erb +0 -10
  87. data/config/locales/ja-JP.yml +0 -120
@@ -0,0 +1,29 @@
1
+ <div class="reveal edit-comment-modal" id="<%= "editCommentModal#{model.id}" %>" data-reveal>
2
+ <div class="reveal__header">
3
+ <h3 class="reveal__title"><%= t("decidim.components.edit_comment_modal_form.title") %></h3>
4
+ <button class="close-button" data-close aria-label="<%= t("decidim.components.edit_comment_modal_form.close") %>" type="button">
5
+ <span aria-hidden="true">&times;</span>
6
+ </button>
7
+ </div>
8
+ <%= form_for(form_object, url: decidim_comments.comment_path(comment), method: :put, remote: true, html: { id: form_id }) do |form| %>
9
+ <div class="field">
10
+ <label class="show-for-sr" for="<%= form_id %>">
11
+ <%= t("decidim.components.edit_comment_modal_form.form.body.label") %>
12
+ </label>
13
+ <div class="hashtags__container">
14
+ <%= form.text_area(
15
+ :body,
16
+ id: form_id,
17
+ rows: 4,
18
+ maxlength: comments_max_length,
19
+ required: true,
20
+ placeholder: t("decidim.components.edit_comment_modal_form.form.body.placeholder"),
21
+ label: false,
22
+ data: { remaining_characters: "##{form_id}-remaining-characters" }
23
+ ) %>
24
+ </div>
25
+ <button type="submit" data-close class="button button--sc"><%= t("decidim.components.edit_comment_modal_form.form.submit") %></button>
26
+ <span id="<%= form_id %>-remaining-characters" class="remaining-character-count"></span>
27
+ </div>
28
+ <% end %>
29
+ </div>
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Comments
5
+ # A cell to display a form for edditing a comment.
6
+ class EditCommentModalFormCell < Decidim::ViewModel
7
+ delegate :current_user, :user_signed_in?, to: :controller
8
+ alias comment model
9
+
10
+ private
11
+
12
+ def decidim_comments
13
+ Decidim::Comments::Engine.routes.url_helpers
14
+ end
15
+
16
+ def form_id
17
+ "edit_comment_#{comment.id}"
18
+ end
19
+
20
+ def form_object
21
+ Decidim::Comments::CommentForm.new(
22
+ body: comment.translated_body
23
+ )
24
+ end
25
+
26
+ def comments_max_length
27
+ return 1000 unless model.respond_to?(:component)
28
+ return component_comments_max_length if component_comments_max_length
29
+ return organization_comments_max_length if organization_comments_max_length
30
+
31
+ 1000
32
+ end
33
+
34
+ def component_comments_max_length
35
+ return unless model.component&.settings.respond_to?(:comments_max_length)
36
+
37
+ model.component.settings.comments_max_length if model.component.settings.comments_max_length.positive?
38
+ end
39
+
40
+ def organization_comments_max_length
41
+ return unless organization
42
+
43
+ organization.comments_max_length if organization.comments_max_length.positive?
44
+ end
45
+
46
+ def organization
47
+ return model.organization if model.respond_to?(:organization)
48
+
49
+ model.component.organization if model.component.organization.comments_max_length.positive?
50
+ end
51
+ end
52
+ end
53
+ end
@@ -39,7 +39,8 @@ module Decidim
39
39
  root_commentable: root_commentable(form.commentable),
40
40
  body: { I18n.locale => parsed.rewrite },
41
41
  alignment: form.alignment,
42
- decidim_user_group_id: form.user_group_id
42
+ decidim_user_group_id: form.user_group_id,
43
+ participatory_space: form.current_component.try(:participatory_space)
43
44
  }
44
45
 
45
46
  @comment = Decidim.traceability.create!(
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Comments
5
+ # A command with all the business logic to delete a comment
6
+ class DeleteComment < Rectify::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # comment - The comment to delete.
10
+ # current_user - The user performing the action.
11
+ def initialize(comment, current_user)
12
+ @comment = comment
13
+ @current_user = current_user
14
+ end
15
+
16
+ # Executes the command. Broadcasts these events:
17
+ #
18
+ # - :ok when everything is valid.
19
+ # - :invalid if comment isn't authored by current_user.
20
+ #
21
+ # Returns nothing.
22
+ def call
23
+ return broadcast(:invalid) unless comment.authored_by?(current_user)
24
+
25
+ delete_comment
26
+
27
+ broadcast(:ok)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :comment, :current_user
33
+
34
+ def delete_comment
35
+ Decidim.traceability.perform_action!(
36
+ :delete,
37
+ comment,
38
+ current_user,
39
+ visibility: "public-only"
40
+ ) do
41
+ comment.delete!
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Comments
5
+ # A command with all the business logic to update an existing comment
6
+ class UpdateComment < Rectify::Command
7
+ # Public: Initializes the command.
8
+ #
9
+ # comment - Decidim::Comments::Comment
10
+ # current_user - Decidim::User
11
+ # form - A form object with the params.
12
+ def initialize(comment, current_user, form)
13
+ @comment = comment
14
+ @current_user = current_user
15
+ @form = form
16
+ end
17
+
18
+ # Executes the command. Broadcasts these events:
19
+ #
20
+ # - :ok when everything is valid.
21
+ # - :invalid if the form wasn't valid and we couldn't proceed.
22
+ #
23
+ # Returns nothing.
24
+ def call
25
+ return broadcast(:invalid) if form.invalid? || !comment.authored_by?(current_user)
26
+
27
+ update_comment
28
+
29
+ broadcast(:ok)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :form, :comment, :current_user
35
+
36
+ def update_comment
37
+ parsed = Decidim::ContentProcessor.parse(form.body, current_organization: form.current_organization)
38
+
39
+ params = {
40
+ body: { I18n.locale => parsed.rewrite }
41
+ }
42
+
43
+ @comment = Decidim.traceability.update!(
44
+ comment,
45
+ current_user,
46
+ params,
47
+ visibility: "public-only",
48
+ edit: true
49
+ )
50
+
51
+ mentioned_users = parsed.metadata[:user].users
52
+ mentioned_groups = parsed.metadata[:user_group].groups
53
+ CommentCreation.publish(@comment, parsed.metadata)
54
+ send_notifications(mentioned_users, mentioned_groups)
55
+ end
56
+
57
+ def send_notifications(mentioned_users, mentioned_groups)
58
+ NewCommentNotificationCreator.new(comment, mentioned_users, mentioned_groups).create
59
+ end
60
+ end
61
+ end
62
+ end
@@ -8,8 +8,8 @@ module Decidim
8
8
  include Decidim::ResourceHelper
9
9
 
10
10
  before_action :authenticate_user!, only: [:create]
11
- before_action :set_commentable
12
- before_action :ensure_commentable!
11
+ before_action :set_commentable, except: [:destroy, :update]
12
+ before_action :ensure_commentable!, except: [:destroy, :update]
13
13
 
14
14
  helper_method :root_depth, :commentable, :order, :reply?, :reload?
15
15
 
@@ -21,7 +21,7 @@ module Decidim
21
21
  order_by: order,
22
22
  after: params.fetch(:after, 0).to_i
23
23
  )
24
- @comments_count = commentable.comments.count
24
+ @comments_count = commentable.comments_count
25
25
 
26
26
  respond_to do |format|
27
27
  format.js do
@@ -37,6 +37,31 @@ module Decidim
37
37
  end
38
38
  end
39
39
 
40
+ def update
41
+ set_comment
42
+ enforce_permission_to :update, :comment, comment: comment
43
+
44
+ form = Decidim::Comments::CommentForm.from_params(
45
+ params.merge(commentable: comment.commentable)
46
+ ).with_context(
47
+ current_organization: current_organization
48
+ )
49
+
50
+ Decidim::Comments::UpdateComment.call(comment, current_user, form) do
51
+ on(:ok) do
52
+ respond_to do |format|
53
+ format.js { render :update }
54
+ end
55
+ end
56
+
57
+ on(:invalid) do
58
+ respond_to do |format|
59
+ format.js { render :update_error }
60
+ end
61
+ end
62
+ end
63
+ end
64
+
40
65
  def create
41
66
  enforce_permission_to :create, :comment, commentable: commentable
42
67
 
@@ -44,7 +69,7 @@ module Decidim
44
69
  params.merge(commentable: commentable)
45
70
  ).with_context(
46
71
  current_organization: current_organization,
47
- current_component: commentable.component
72
+ current_component: current_component
48
73
  )
49
74
  Decidim::Comments::CreateComment.call(form, current_user) do
50
75
  on(:ok) do |comment|
@@ -63,6 +88,34 @@ module Decidim
63
88
  end
64
89
  end
65
90
 
91
+ def current_component
92
+ return commentable.component if commentable.respond_to?(:component)
93
+ return commentable.participatory_space if commentable.respond_to?(:participatory_space)
94
+ return commentable if Decidim.participatory_space_manifests.find { |manifest| manifest.model_class_name == commentable.class.name }
95
+ end
96
+
97
+ def destroy
98
+ set_comment
99
+ @commentable = @comment.commentable
100
+
101
+ enforce_permission_to :destroy, :comment, comment: comment
102
+
103
+ Decidim::Comments::DeleteComment.call(comment, current_user) do
104
+ on(:ok) do
105
+ @comments_count = @comment.root_commentable.comments_count
106
+ respond_to do |format|
107
+ format.js { render :delete }
108
+ end
109
+ end
110
+
111
+ on(:invalid) do
112
+ respond_to do |format|
113
+ format.js { render :deletion_error }
114
+ end
115
+ end
116
+ end
117
+ end
118
+
66
119
  private
67
120
 
68
121
  attr_reader :commentable, :comment
@@ -71,6 +124,10 @@ module Decidim
71
124
  @commentable = GlobalID::Locator.locate_signed(commentable_gid)
72
125
  end
73
126
 
127
+ def set_comment
128
+ @comment = Decidim::Comments::Comment.find_by(id: params[:id])
129
+ end
130
+
74
131
  def ensure_commentable!
75
132
  raise ActionController::RoutingError, "Not Found" unless commentable
76
133
  end
@@ -80,9 +137,9 @@ module Decidim
80
137
  @comments_count = begin
81
138
  case commentable
82
139
  when Decidim::Comments::Comment
83
- commentable.root_commentable.comments.count
140
+ commentable.root_commentable.comments_count
84
141
  else
85
- commentable.comments.count
142
+ commentable.comments_count
86
143
  end
87
144
  end
88
145
  end
@@ -8,6 +8,11 @@ module Decidim
8
8
  i18n_attributes :upvotes
9
9
  i18n_attributes :downvotes
10
10
 
11
+ def initialize(resource:, event_name:, user:, user_role: nil, extra: nil)
12
+ resource = target_resource(resource)
13
+ super
14
+ end
15
+
11
16
  def upvotes
12
17
  extra[:upvotes]
13
18
  end
@@ -21,6 +26,10 @@ module Decidim
21
26
  def resource_url_params
22
27
  { anchor: "comment_#{comment.id}" }
23
28
  end
29
+
30
+ def target_resource(t_resource)
31
+ t_resource.is_a?(Decidim::Comments::Comment) ? t_resource.root_commentable : t_resource
32
+ end
24
33
  end
25
34
  end
26
35
  end
@@ -19,7 +19,7 @@ module Decidim
19
19
  validate :max_depth
20
20
 
21
21
  def max_length
22
- if current_component&.settings.respond_to?(:comments_max_length)
22
+ if current_component.try(:settings).respond_to?(:comments_max_length)
23
23
  component_length = current_component.try { settings.comments_max_length.positive? }
24
24
  return current_component.settings.comments_max_length if component_length
25
25
  end
@@ -29,6 +29,7 @@ module Decidim
29
29
 
30
30
  belongs_to :commentable, foreign_key: "decidim_commentable_id", foreign_type: "decidim_commentable_type", polymorphic: true
31
31
  belongs_to :root_commentable, foreign_key: "decidim_root_commentable_id", foreign_type: "decidim_root_commentable_type", polymorphic: true, touch: true
32
+ belongs_to :participatory_space, foreign_key: "decidim_participatory_space_id", foreign_type: "decidim_participatory_space_type", polymorphic: true, optional: true
32
33
  has_many :up_votes, -> { where(weight: 1) }, foreign_key: "decidim_comment_id", class_name: "CommentVote", dependent: :destroy
33
34
  has_many :down_votes, -> { where(weight: -1) }, foreign_key: "decidim_comment_id", class_name: "CommentVote", dependent: :destroy
34
35
 
@@ -54,6 +55,8 @@ module Decidim
54
55
 
55
56
  delegate :organization, to: :commentable
56
57
 
58
+ scope :not_deleted, -> { where(deleted_at: nil) }
59
+
57
60
  translatable_fields :body
58
61
  searchable_fields({
59
62
  participatory_space: :itself,
@@ -79,7 +82,9 @@ module Decidim
79
82
  participatory_space.try(:visible?) && component.try(:published?)
80
83
  end
81
84
 
85
+ alias original_participatory_space participatory_space
82
86
  def participatory_space
87
+ return original_participatory_space if original_participatory_space.present?
83
88
  return root_commentable if root_commentable.is_a?(Decidim::Participable)
84
89
 
85
90
  root_commentable.participatory_space
@@ -91,6 +96,8 @@ module Decidim
91
96
 
92
97
  # Public: Override Commentable concern method `accepts_new_comments?`
93
98
  def accepts_new_comments?
99
+ return if deleted?
100
+
94
101
  root_commentable.accepts_new_comments? && depth < MAX_DEPTH
95
102
  end
96
103
 
@@ -156,7 +163,7 @@ module Decidim
156
163
  def self.user_commentators_ids_in(resources)
157
164
  if resources.first&.kind_of?(Decidim::Comments::Commentable)
158
165
  commentable_type = resources.first.class.name
159
- Decidim::Comments::Comment.select("DISTINCT decidim_author_id").not_hidden
166
+ Decidim::Comments::Comment.select("DISTINCT decidim_author_id").not_hidden.not_deleted
160
167
  .where(decidim_commentable_id: resources.pluck(:id))
161
168
  .where(decidim_commentable_type: commentable_type)
162
169
  .where("decidim_author_type" => "Decidim::UserBaseEntity").pluck(:decidim_author_id)
@@ -179,6 +186,22 @@ module Decidim
179
186
  @translated_body ||= translated_attribute(body, organization)
180
187
  end
181
188
 
189
+ def delete!
190
+ return if deleted?
191
+
192
+ update(deleted_at: Time.current)
193
+
194
+ update_counter
195
+ end
196
+
197
+ def deleted?
198
+ deleted_at.present?
199
+ end
200
+
201
+ def edited?
202
+ Decidim::ActionLog.where(resource: self).exists?(["extra @> ?", Arel.sql("{\"edit\":true}")])
203
+ end
204
+
182
205
  private
183
206
 
184
207
  def body_length
@@ -0,0 +1,300 @@
1
+ /* eslint id-length: ["error", { "exceptions": ["$"] }] */
2
+
3
+ /**
4
+ * A plain Javascript component that handles the comments.
5
+ *
6
+ * @class
7
+ * @augments Component
8
+ */
9
+
10
+ // This is necessary for testing purposes
11
+ const $ = window.$;
12
+
13
+ import { createCharacterCounter } from "src/decidim/input_character_counter"
14
+ import ExternalLink from "src/decidim/external_link"
15
+ import updateExternalDomainLinks from "src/decidim/external_domain_warning"
16
+
17
+ export default class CommentsComponent {
18
+ constructor($element, config) {
19
+ this.$element = $element;
20
+ this.commentableGid = config.commentableGid;
21
+ this.commentsUrl = config.commentsUrl;
22
+ this.rootDepth = config.rootDepth;
23
+ this.order = config.order;
24
+ this.lastCommentId = config.lastCommentId;
25
+ this.pollingInterval = config.pollingInterval || 15000;
26
+ this.id = this.$element.attr("id") || this._getUID();
27
+ this.mounted = false;
28
+ }
29
+
30
+ /**
31
+ * Handles the logic for mounting the component
32
+ * @public
33
+ * @returns {Void} - Returns nothing
34
+ */
35
+ mountComponent() {
36
+ if (this.$element.length > 0 && !this.mounted) {
37
+ this.mounted = true;
38
+ this._initializeComments(this.$element);
39
+
40
+ $(".order-by__dropdown .is-submenu-item a", this.$element).on("click.decidim-comments", () => this._onInitOrder());
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Handles the logic for unmounting the component
46
+ * @public
47
+ * @returns {Void} - Returns nothing
48
+ */
49
+ unmountComponent() {
50
+ if (this.mounted) {
51
+ this.mounted = false;
52
+ this._stopPolling();
53
+
54
+ $(".add-comment .opinion-toggle .button", this.$element).off("click.decidim-comments");
55
+ $(".add-comment textarea", this.$element).off("input.decidim-comments");
56
+ $(".order-by__dropdown .is-submenu-item a", this.$element).off("click.decidim-comments");
57
+ $(".add-comment form", this.$element).off("submit.decidim-comments");
58
+ $(".add-comment textarea", this.$element).each((_i, el) => el.removeEventListener("emoji.added", this._onTextInput));
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Adds a new thread to the comments section.
64
+ * @public
65
+ * @param {String} threadHtml - The HTML content for the thread.
66
+ * @param {Boolean} fromCurrentUser - A boolean indicating whether the user
67
+ * herself was the author of the new thread. Defaults to false.
68
+ * @returns {Void} - Returns nothing
69
+ */
70
+ addThread(threadHtml, fromCurrentUser = false) {
71
+ const $parent = $(".comments:first", this.$element);
72
+ const $comment = $(threadHtml);
73
+ const $threads = $(".comment-threads", this.$element);
74
+ this._addComment($threads, $comment);
75
+ this._finalizeCommentCreation($parent, fromCurrentUser);
76
+ }
77
+
78
+ /**
79
+ * Adds a new reply to an existing comment.
80
+ * @public
81
+ * @param {Number} commentId - The ID of the comment for which to add the
82
+ * reply to.
83
+ * @param {String} replyHtml - The HTML content for the reply.
84
+ * @param {Boolean} fromCurrentUser - A boolean indicating whether the user
85
+ * herself was the author of the new reply. Defaults to false.
86
+ * @returns {Void} - Returns nothing
87
+ */
88
+ addReply(commentId, replyHtml, fromCurrentUser = false) {
89
+ const $parent = $(`#comment_${commentId}`);
90
+ const $comment = $(replyHtml);
91
+ const $replies = $(`#comment-${commentId}-replies`);
92
+ this._addComment($replies, $comment);
93
+ $replies.siblings(".comment__additionalreply").removeClass("hide");
94
+ this._finalizeCommentCreation($parent, fromCurrentUser);
95
+ }
96
+
97
+ /**
98
+ * Generates a unique identifier for the form.
99
+ * @private
100
+ * @returns {String} - Returns a unique identifier
101
+ */
102
+ _getUID() {
103
+ return `comments-${new Date().setUTCMilliseconds()}-${Math.floor(Math.random() * 10000000)}`;
104
+ }
105
+
106
+ /**
107
+ * Initializes the comments for the given parent element.
108
+ * @private
109
+ * @param {jQuery} $parent The parent element to initialize.
110
+ * @returns {Void} - Returns nothing
111
+ */
112
+ _initializeComments($parent) {
113
+ $(".add-comment", $parent).each((_i, el) => {
114
+ const $add = $(el);
115
+ const $form = $("form", $add);
116
+ const $opinionButtons = $(".opinion-toggle .button", $add);
117
+ const $text = $("textarea", $form);
118
+
119
+ $opinionButtons.on("click.decidim-comments", this._onToggleOpinion);
120
+ $text.on("input.decidim-comments", this._onTextInput);
121
+
122
+ $(document).trigger("attach-mentions-element", [$text.get(0)]);
123
+
124
+ $form.on("submit.decidim-comments", () => {
125
+ const $submit = $("button[type='submit']", $form);
126
+
127
+ $submit.attr("disabled", "disabled");
128
+ this._stopPolling();
129
+ });
130
+
131
+ if ($text.length && $text.get(0) !== null) {
132
+ // Attach event to the DOM node, instead of the jQuery object
133
+ $text.get(0).addEventListener("emoji.added", this._onTextInput);
134
+ }
135
+ });
136
+
137
+ this._pollComments();
138
+ }
139
+
140
+ /**
141
+ * Adds the given comment element to the given target element and
142
+ * initializes it.
143
+ * @private
144
+ * @param {jQuery} $target - The target element to add the comment to.
145
+ * @param {jQuery} $container - The comment container element to add.
146
+ * @returns {Void} - Returns nothing
147
+ */
148
+ _addComment($target, $container) {
149
+ let $comment = $(".comment", $container);
150
+ if ($comment.length < 1) {
151
+ // In case of a reply
152
+ $comment = $container;
153
+ }
154
+ this.lastCommentId = parseInt($comment.data("comment-id"), 10);
155
+
156
+ $target.append($container);
157
+ $container.foundation();
158
+ this._initializeComments($container);
159
+ createCharacterCounter($(".add-comment textarea", $container));
160
+ $container.find('a[target="_blank"]').each((_i, elem) => {
161
+ const $link = $(elem);
162
+ $link.data("external-link", new ExternalLink($link));
163
+ });
164
+ updateExternalDomainLinks($container)
165
+ }
166
+
167
+ /**
168
+ * Finalizes the new comment creation after the comment adding finishes
169
+ * successfully.
170
+ * @private
171
+ * @param {jQuery} $parent - The parent comment element to finalize.
172
+ * @param {Boolean} fromCurrentUser - A boolean indicating whether the user
173
+ * herself was the author of the new comment.
174
+ * @returns {Void} - Returns nothing
175
+ */
176
+ _finalizeCommentCreation($parent, fromCurrentUser) {
177
+ if (fromCurrentUser) {
178
+ const $add = $("> .add-comment", $parent);
179
+ const $text = $("textarea", $add);
180
+ const characterCounter = $text.data("remaining-characters-counter");
181
+ $text.val("");
182
+ if (characterCounter) {
183
+ characterCounter.updateStatus();
184
+ }
185
+ if (!$add.parent().is(".comments")) {
186
+ $add.addClass("hide");
187
+ }
188
+ }
189
+
190
+ // Restart the polling
191
+ this._pollComments();
192
+ }
193
+
194
+ /**
195
+ * Sets a timeout to poll new comments.
196
+ * @private
197
+ * @returns {Void} - Returns nothing
198
+ */
199
+ _pollComments() {
200
+ this._stopPolling();
201
+
202
+ this.pollTimeout = setTimeout(() => {
203
+ $.ajax({
204
+ url: this.commentsUrl,
205
+ method: "GET",
206
+ contentType: "application/javascript",
207
+ data: {
208
+ "commentable_gid": this.commentableGid,
209
+ "root_depth": this.rootDepth,
210
+ order: this.order,
211
+ after: this.lastCommentId
212
+ }
213
+ }).done(() => this._pollComments());
214
+ }, this.pollingInterval);
215
+ }
216
+
217
+ /**
218
+ * Stops polling for new comments.
219
+ * @private
220
+ * @returns {Void} - Returns nothing
221
+ */
222
+ _stopPolling() {
223
+ if (this.pollTimeout) {
224
+ clearTimeout(this.pollTimeout);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Sets the loading comments element visible in the view.
230
+ * @private
231
+ * @returns {Void} - Returns nothing
232
+ */
233
+ _setLoading() {
234
+ const $container = $("> .comments-container", this.$element);
235
+ $("> .comments", $container).addClass("hide");
236
+ $("> .loading-comments", $container).removeClass("hide");
237
+ }
238
+
239
+ /**
240
+ * Event listener for the ordering links.
241
+ * @private
242
+ * @returns {Void} - Returns nothing
243
+ */
244
+ _onInitOrder() {
245
+ this._stopPolling();
246
+ this._setLoading();
247
+ }
248
+
249
+ /**
250
+ * Event listener for the opinion toggle buttons.
251
+ * @private
252
+ * @param {Event} ev - The event object.
253
+ * @returns {Void} - Returns nothing
254
+ */
255
+ _onToggleOpinion(ev) {
256
+ let $btn = $(ev.target);
257
+ if (!$btn.is(".button")) {
258
+ $btn = $btn.parents(".button");
259
+ }
260
+
261
+ const $add = $btn.closest(".add-comment");
262
+ const $form = $("form", $add);
263
+ const $opinionButtons = $(".opinion-toggle .button", $add);
264
+ const $selectedState = $(".opinion-toggle .selected-state", $add);
265
+ const $alignment = $(".alignment-input", $form);
266
+
267
+ $opinionButtons.removeClass("is-active").attr("aria-pressed", "false");
268
+ $btn.addClass("is-active").attr("aria-pressed", "true");
269
+
270
+ if ($btn.is(".opinion-toggle--ok")) {
271
+ $alignment.val(1);
272
+ } else if ($btn.is(".opinion-toggle--meh")) {
273
+ $alignment.val(0);
274
+ } else if ($btn.is(".opinion-toggle--ko")) {
275
+ $alignment.val(-1);
276
+ }
277
+
278
+ // Announce the selected state for the screen reader
279
+ $selectedState.text($btn.data("selected-label"));
280
+ }
281
+
282
+ /**
283
+ * Event listener for the comment field text input.
284
+ * @private
285
+ * @param {Event} ev - The event object.
286
+ * @returns {Void} - Returns nothing
287
+ */
288
+ _onTextInput(ev) {
289
+ const $text = $(ev.target);
290
+ const $add = $text.closest(".add-comment");
291
+ const $form = $("form", $add);
292
+ const $submit = $("button[type='submit']", $form);
293
+
294
+ if ($text.val().length > 0) {
295
+ $submit.removeAttr("disabled");
296
+ } else {
297
+ $submit.attr("disabled", "disabled");
298
+ }
299
+ }
300
+ }