decidim-comments 0.24.3 → 0.25.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -20
  3. data/app/cells/decidim/comments/comment/actions.erb +1 -1
  4. data/app/cells/decidim/comments/comment/deletion_data.erb +1 -0
  5. data/app/cells/decidim/comments/comment/show.erb +30 -21
  6. data/app/cells/decidim/comments/comment/utilities.erb +40 -12
  7. data/app/cells/decidim/comments/comment/votes.erb +6 -6
  8. data/app/cells/decidim/comments/comment_cell.rb +29 -0
  9. data/app/cells/decidim/comments/comment_form/show.erb +1 -1
  10. data/app/cells/decidim/comments/comments/add_comment.erb +10 -6
  11. data/app/cells/decidim/comments/comments/order_control.erb +4 -5
  12. data/app/cells/decidim/comments/comments/show.erb +2 -4
  13. data/app/cells/decidim/comments/comments/user_comments_blocked_warning.erb +5 -1
  14. data/app/cells/decidim/comments/comments_cell.rb +24 -2
  15. data/app/cells/decidim/comments/edit_comment_modal_form/show.erb +29 -0
  16. data/app/cells/decidim/comments/edit_comment_modal_form_cell.rb +53 -0
  17. data/app/commands/decidim/comments/delete_comment.rb +46 -0
  18. data/app/commands/decidim/comments/update_comment.rb +62 -0
  19. data/app/controllers/decidim/comments/comments_controller.rb +63 -6
  20. data/app/events/decidim/comments/comment_voted_event.rb +9 -0
  21. data/app/models/decidim/comments/comment.rb +21 -1
  22. data/app/packs/src/decidim/comments/comments.component.js +299 -0
  23. data/app/{assets/javascripts → packs/src}/decidim/comments/comments.component.test.js +49 -24
  24. data/app/packs/src/decidim/comments/comments.component_for_testing.js +8 -0
  25. data/app/packs/src/decidim/comments/comments.js +1 -0
  26. data/app/permissions/decidim/comments/permissions.rb +10 -1
  27. data/app/queries/decidim/comments/metrics/comment_participants_metric_measure.rb +1 -1
  28. data/app/queries/decidim/comments/metrics/comments_metric_manage.rb +1 -1
  29. data/app/views/decidim/comments/comments/_delete.html.erb +5 -0
  30. data/app/views/decidim/comments/comments/_edited_comment.html.erb +1 -0
  31. data/app/views/decidim/comments/comments/create.js.erb +2 -0
  32. data/app/views/decidim/comments/comments/delete.js.erb +17 -0
  33. data/app/views/decidim/comments/comments/deletion_error.js.erb +1 -0
  34. data/app/views/decidim/comments/comments/reload.js.erb +2 -0
  35. data/app/views/decidim/comments/comments/update.js.erb +8 -0
  36. data/app/views/decidim/comments/comments/update_error.js.erb +1 -0
  37. data/config/assets.rb +5 -0
  38. data/config/locales/ar.yml +0 -1
  39. data/config/locales/ca.yml +7 -1
  40. data/config/locales/cs.yml +25 -1
  41. data/config/locales/de.yml +7 -1
  42. data/config/locales/el.yml +0 -1
  43. data/config/locales/en.yml +25 -1
  44. data/config/locales/es-MX.yml +7 -1
  45. data/config/locales/es-PY.yml +7 -1
  46. data/config/locales/es.yml +7 -1
  47. data/config/locales/fi-plain.yml +25 -1
  48. data/config/locales/fi.yml +25 -1
  49. data/config/locales/fr-CA.yml +25 -1
  50. data/config/locales/fr-LU.yml +162 -0
  51. data/config/locales/fr.yml +25 -1
  52. data/config/locales/gl.yml +25 -1
  53. data/config/locales/hu.yml +0 -1
  54. data/config/locales/it.yml +38 -1
  55. data/config/locales/ja.yml +35 -1
  56. data/config/locales/lb-LU.yml +1 -0
  57. data/config/locales/lv.yml +0 -1
  58. data/config/locales/nl.yml +27 -2
  59. data/config/locales/no.yml +0 -1
  60. data/config/locales/pl.yml +7 -1
  61. data/config/locales/pt-BR.yml +61 -0
  62. data/config/locales/pt.yml +0 -1
  63. data/config/locales/ro-RO.yml +25 -1
  64. data/config/locales/sk.yml +0 -1
  65. data/config/locales/sr-CS.yml +0 -1
  66. data/config/locales/sv.yml +25 -1
  67. data/config/locales/tr-TR.yml +0 -1
  68. data/config/locales/zh-CN.yml +0 -1
  69. data/db/migrate/20200706123136_make_comments_handle_i18n.rb +1 -1
  70. data/db/migrate/20210529095942_add_deleted_at_column_to_comments.rb +7 -0
  71. data/lib/decidim/comments.rb +1 -0
  72. data/lib/decidim/comments/commentable.rb +6 -1
  73. data/lib/decidim/comments/commentable_with_component.rb +33 -0
  74. data/lib/decidim/comments/engine.rb +1 -9
  75. data/lib/decidim/comments/version.rb +1 -1
  76. metadata +31 -31
  77. data/app/assets/config/decidim_comments_manifest.js +0 -1
  78. data/app/assets/javascripts/decidim/comments/bundle.js.map +0 -1
  79. data/app/assets/javascripts/decidim/comments/comments.component.js.es6 +0 -292
  80. data/app/assets/javascripts/decidim/comments/comments.js.erb +0 -10
  81. 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
@@ -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.try(:component) || commentable.participatory_space
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
@@ -54,6 +54,8 @@ module Decidim
54
54
 
55
55
  delegate :organization, to: :commentable
56
56
 
57
+ scope :not_deleted, -> { where(deleted_at: nil) }
58
+
57
59
  translatable_fields :body
58
60
  searchable_fields({
59
61
  participatory_space: :itself,
@@ -91,6 +93,8 @@ module Decidim
91
93
 
92
94
  # Public: Override Commentable concern method `accepts_new_comments?`
93
95
  def accepts_new_comments?
96
+ return if deleted?
97
+
94
98
  root_commentable.accepts_new_comments? && depth < MAX_DEPTH
95
99
  end
96
100
 
@@ -156,7 +160,7 @@ module Decidim
156
160
  def self.user_commentators_ids_in(resources)
157
161
  if resources.first&.kind_of?(Decidim::Comments::Commentable)
158
162
  commentable_type = resources.first.class.name
159
- Decidim::Comments::Comment.select("DISTINCT decidim_author_id").not_hidden
163
+ Decidim::Comments::Comment.select("DISTINCT decidim_author_id").not_hidden.not_deleted
160
164
  .where(decidim_commentable_id: resources.pluck(:id))
161
165
  .where(decidim_commentable_type: commentable_type)
162
166
  .where("decidim_author_type" => "Decidim::UserBaseEntity").pluck(:decidim_author_id)
@@ -179,6 +183,22 @@ module Decidim
179
183
  @translated_body ||= translated_attribute(body, organization)
180
184
  end
181
185
 
186
+ def delete!
187
+ return if deleted?
188
+
189
+ update(deleted_at: Time.current)
190
+
191
+ update_counter
192
+ end
193
+
194
+ def deleted?
195
+ deleted_at.present?
196
+ end
197
+
198
+ def edited?
199
+ Decidim::ActionLog.where(resource: self).exists?(["extra @> ?", Arel.sql("{\"edit\":true}")])
200
+ end
201
+
182
202
  private
183
203
 
184
204
  def body_length
@@ -0,0 +1,299 @@
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(
41
+ "click.decidim-comments",
42
+ () => {
43
+ this._onInitOrder();
44
+ }
45
+ );
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Handles the logic for unmounting the component
51
+ * @public
52
+ * @returns {Void} - Returns nothing
53
+ */
54
+ unmountComponent() {
55
+ if (this.mounted) {
56
+ this.mounted = false;
57
+ this._stopPolling();
58
+
59
+ $(".add-comment .opinion-toggle .button", this.$element).off("click.decidim-comments");
60
+ $(".add-comment textarea", this.$element).off("input.decidim-comments");
61
+ $(".order-by__dropdown .is-submenu-item a", this.$element).off("click.decidim-comments");
62
+ $(".add-comment form", this.$element).off("submit.decidim-comments");
63
+ $(".add-comment textarea", this.$element).each((_i, el) => el.removeEventListener("emoji.added", this._onTextInput));
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Adds a new thread to the comments section.
69
+ * @public
70
+ * @param {String} threadHtml - The HTML content for the thread.
71
+ * @returns {Void} - Returns nothing
72
+ */
73
+ addThread(threadHtml) {
74
+ const $parent = $(".comments:first", this.$element);
75
+ const $comment = $(threadHtml);
76
+ const $threads = $(".comment-threads", this.$element);
77
+ this._addComment($threads, $comment);
78
+ this._finalizeCommentCreation($parent);
79
+ }
80
+
81
+ /**
82
+ * Adds a new reply to an existing comment.
83
+ * @public
84
+ * @param {Number} commentId - The ID of the comment for which to add the
85
+ * reply to.
86
+ * @param {String} replyHtml - The HTML content for the reply.
87
+ * @returns {Void} - Returns nothing
88
+ */
89
+ addReply(commentId, replyHtml) {
90
+ const $parent = $(`#comment_${commentId}`);
91
+ const $comment = $(replyHtml);
92
+ const $replies = $(`#comment-${commentId}-replies`);
93
+ this._addComment($replies, $comment);
94
+ $replies.siblings(".comment__additionalreply").removeClass("hide");
95
+ this._finalizeCommentCreation($parent);
96
+ }
97
+
98
+ /**
99
+ * Generates a unique identifier for the form.
100
+ * @private
101
+ * @returns {String} - Returns a unique identifier
102
+ */
103
+ _getUID() {
104
+ return `comments-${new Date().setUTCMilliseconds()}-${Math.floor(Math.random() * 10000000)}`;
105
+ }
106
+
107
+ /**
108
+ * Initializes the comments for the given parent element.
109
+ * @private
110
+ * @param {jQuery} $parent The parent element to initialize.
111
+ * @returns {Void} - Returns nothing
112
+ */
113
+ _initializeComments($parent) {
114
+ $(".add-comment", $parent).each((_i, el) => {
115
+ const $add = $(el);
116
+ const $form = $("form", $add);
117
+ const $opinionButtons = $(".opinion-toggle .button", $add);
118
+ const $text = $("textarea", $form);
119
+
120
+ $opinionButtons.on("click.decidim-comments", this._onToggleOpinion);
121
+ $text.on("input.decidim-comments", this._onTextInput);
122
+
123
+ $(document).trigger("attach-mentions-element", [$text.get(0)]);
124
+
125
+ $form.on("submit.decidim-comments", () => {
126
+ const $submit = $("button[type='submit']", $form);
127
+
128
+ $submit.attr("disabled", "disabled");
129
+ this._stopPolling();
130
+ });
131
+
132
+ if ($text.length && $text.get(0) !== null) {
133
+ // Attach event to the DOM node, instead of the jQuery object
134
+ $text.get(0).addEventListener("emoji.added", this._onTextInput);
135
+ }
136
+ });
137
+
138
+ this._pollComments();
139
+ }
140
+
141
+ /**
142
+ * Adds the given comment element to the given target element and
143
+ * initializes it.
144
+ * @private
145
+ * @param {jQuery} $target - The target element to add the comment to.
146
+ * @param {jQuery} $container - The comment container element to add.
147
+ * @returns {Void} - Returns nothing
148
+ */
149
+ _addComment($target, $container) {
150
+ let $comment = $(".comment", $container);
151
+ if ($comment.length < 1) {
152
+ // In case of a reply
153
+ $comment = $container;
154
+ }
155
+ this.lastCommentId = parseInt($comment.data("comment-id"), 10);
156
+
157
+ $target.append($container);
158
+ $container.foundation();
159
+ this._initializeComments($container);
160
+ createCharacterCounter($(".add-comment textarea", $container));
161
+ $container.find('a[target="_blank"]').each((_i, elem) => {
162
+ const $link = $(elem);
163
+ $link.data("external-link", new ExternalLink($link));
164
+ });
165
+ updateExternalDomainLinks($container)
166
+ }
167
+
168
+ /**
169
+ * Finalizes the new comment creation after the comment adding finishes
170
+ * successfully.
171
+ * @private
172
+ * @param {jQuery} $parent - The parent comment element to finalize.
173
+ * @returns {Void} - Returns nothing
174
+ */
175
+ _finalizeCommentCreation($parent) {
176
+ const $add = $("> .add-comment", $parent);
177
+ const $text = $("textarea", $add);
178
+ const characterCounter = $text.data("remaining-characters-counter");
179
+ $text.val("");
180
+ if (characterCounter) {
181
+ characterCounter.updateStatus();
182
+ }
183
+ if (!$add.parent().is(".comments")) {
184
+ $add.addClass("hide");
185
+ }
186
+
187
+ // Restart the polling
188
+ this._pollComments();
189
+ }
190
+
191
+ /**
192
+ * Sets a timeout to poll new comments.
193
+ * @private
194
+ * @returns {Void} - Returns nothing
195
+ */
196
+ _pollComments() {
197
+ this._stopPolling();
198
+
199
+ this.pollTimeout = setTimeout(() => {
200
+ $.ajax({
201
+ url: this.commentsUrl,
202
+ method: "GET",
203
+ contentType: "application/javascript",
204
+ data: {
205
+ "commentable_gid": this.commentableGid,
206
+ "root_depth": this.rootDepth,
207
+ order: this.order,
208
+ after: this.lastCommentId
209
+ }
210
+ }).done(() => {
211
+ this._pollComments();
212
+ });
213
+ }, this.pollingInterval);
214
+ }
215
+
216
+ /**
217
+ * Stops polling for new comments.
218
+ * @private
219
+ * @returns {Void} - Returns nothing
220
+ */
221
+ _stopPolling() {
222
+ if (this.pollTimeout) {
223
+ clearTimeout(this.pollTimeout);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Sets the loading comments element visible in the view.
229
+ * @private
230
+ * @returns {Void} - Returns nothing
231
+ */
232
+ _setLoading() {
233
+ const $container = $("> .comments-container", this.$element);
234
+ $("> .comments", $container).addClass("hide");
235
+ $("> .loading-comments", $container).removeClass("hide");
236
+ }
237
+
238
+ /**
239
+ * Event listener for the ordering links.
240
+ * @private
241
+ * @returns {Void} - Returns nothing
242
+ */
243
+ _onInitOrder() {
244
+ this._stopPolling();
245
+ this._setLoading();
246
+ }
247
+
248
+ /**
249
+ * Event listener for the opinion toggle buttons.
250
+ * @private
251
+ * @param {Event} ev - The event object.
252
+ * @returns {Void} - Returns nothing
253
+ */
254
+ _onToggleOpinion(ev) {
255
+ let $btn = $(ev.target);
256
+ if (!$btn.is(".button")) {
257
+ $btn = $btn.parents(".button");
258
+ }
259
+
260
+ const $add = $btn.closest(".add-comment");
261
+ const $form = $("form", $add);
262
+ const $opinionButtons = $(".opinion-toggle .button", $add);
263
+ const $selectedState = $(".opinion-toggle .selected-state", $add);
264
+ const $alignment = $(".alignment-input", $form);
265
+
266
+ $opinionButtons.removeClass("is-active").attr("aria-pressed", "false");
267
+ $btn.addClass("is-active").attr("aria-pressed", "true");
268
+
269
+ if ($btn.is(".opinion-toggle--ok")) {
270
+ $alignment.val(1);
271
+ } else if ($btn.is(".opinion-toggle--meh")) {
272
+ $alignment.val(0);
273
+ } else if ($btn.is(".opinion-toggle--ko")) {
274
+ $alignment.val(-1);
275
+ }
276
+
277
+ // Announce the selected state for the screen reader
278
+ $selectedState.text($btn.data("selected-label"));
279
+ }
280
+
281
+ /**
282
+ * Event listener for the comment field text input.
283
+ * @private
284
+ * @param {Event} ev - The event object.
285
+ * @returns {Void} - Returns nothing
286
+ */
287
+ _onTextInput(ev) {
288
+ const $text = $(ev.target);
289
+ const $add = $text.closest(".add-comment");
290
+ const $form = $("form", $add);
291
+ const $submit = $("button[type='submit']", $form);
292
+
293
+ if ($text.val().length > 0) {
294
+ $submit.removeAttr("disabled");
295
+ } else {
296
+ $submit.attr("disabled", "disabled");
297
+ }
298
+ }
299
+ }