decidim-comments 0.19.0 → 0.22.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/decidim/comments/bundle.js +41 -41
  3. data/app/assets/javascripts/decidim/comments/bundle.js.map +1 -1
  4. data/app/cells/decidim/comments/comment_activity_cell.rb +1 -1
  5. data/app/commands/decidim/comments/create_comment.rb +7 -7
  6. data/app/events/decidim/comments/comment_by_followed_user_group_event.rb +9 -0
  7. data/app/events/decidim/comments/comment_event.rb +15 -2
  8. data/app/events/decidim/comments/user_group_mentioned_event.rb +10 -0
  9. data/app/forms/decidim/comments/comment_form.rb +9 -0
  10. data/app/frontend/application/icon.component.tsx +16 -4
  11. data/app/frontend/comments/add_comment_form.component.test.tsx +4 -1
  12. data/app/frontend/comments/add_comment_form.component.tsx +16 -3
  13. data/app/frontend/comments/comment.component.test.tsx +1 -1
  14. data/app/frontend/comments/comment.component.tsx +287 -74
  15. data/app/frontend/comments/comment_order_selector.component.tsx +26 -7
  16. data/app/frontend/comments/comments.component.tsx +65 -8
  17. data/app/frontend/comments/down_vote_button.component.tsx +3 -0
  18. data/app/frontend/comments/up_vote_button.component.tsx +3 -0
  19. data/app/frontend/comments/vote_button.component.tsx +4 -0
  20. data/app/frontend/comments/vote_button_component.test.tsx +14 -8
  21. data/app/frontend/entry.ts +19 -0
  22. data/app/frontend/entry_test.ts +2 -0
  23. data/app/frontend/queries/comments.query.graphql +2 -2
  24. data/app/frontend/support/schema.ts +745 -744
  25. data/app/models/decidim/comments/comment.rb +30 -5
  26. data/app/queries/decidim/comments/metrics/comments_metric_manage.rb +1 -6
  27. data/app/queries/decidim/comments/sorted_comments.rb +8 -2
  28. data/app/scrubbers/decidim/comments/user_input_scrubber.rb +20 -0
  29. data/app/services/decidim/comments/new_comment_notification_creator.rb +28 -3
  30. data/app/types/decidim/comments/commentable_interface.rb +2 -1
  31. data/app/types/decidim/comments/commentable_mutation_type.rb +2 -2
  32. data/config/locales/ar.yml +10 -1
  33. data/config/locales/bg-BG.yml +6 -0
  34. data/config/locales/ca.yml +23 -1
  35. data/config/locales/cs.yml +35 -13
  36. data/config/locales/da-DK.yml +1 -0
  37. data/config/locales/de.yml +23 -1
  38. data/config/locales/el-GR.yml +1 -0
  39. data/config/locales/el.yml +122 -0
  40. data/config/locales/en.yml +23 -1
  41. data/config/locales/es-MX.yml +23 -1
  42. data/config/locales/es-PY.yml +23 -1
  43. data/config/locales/es.yml +23 -1
  44. data/config/locales/et-EE.yml +1 -0
  45. data/config/locales/eu.yml +4 -1
  46. data/config/locales/fi-plain.yml +23 -1
  47. data/config/locales/fi.yml +29 -7
  48. data/config/locales/fr-CA.yml +122 -0
  49. data/config/locales/fr.yml +23 -1
  50. data/config/locales/ga-IE.yml +1 -0
  51. data/config/locales/gl.yml +4 -1
  52. data/config/locales/hr-HR.yml +1 -0
  53. data/config/locales/hu.yml +17 -1
  54. data/config/locales/id-ID.yml +4 -1
  55. data/config/locales/is-IS.yml +76 -0
  56. data/config/locales/it.yml +23 -1
  57. data/config/locales/ja-JP.yml +120 -0
  58. data/config/locales/lt-LT.yml +1 -0
  59. data/config/locales/lv-LV.yml +118 -0
  60. data/config/locales/mt-MT.yml +1 -0
  61. data/config/locales/nl.yml +35 -13
  62. data/config/locales/no.yml +118 -0
  63. data/config/locales/pl.yml +57 -35
  64. data/config/locales/pt-BR.yml +5 -2
  65. data/config/locales/pt.yml +47 -25
  66. data/config/locales/ro-RO.yml +124 -0
  67. data/config/locales/ru.yml +12 -1
  68. data/config/locales/sk-SK.yml +116 -0
  69. data/config/locales/sk.yml +120 -0
  70. data/config/locales/sl.yml +4 -0
  71. data/config/locales/sr-CS.yml +20 -0
  72. data/config/locales/sv.yml +25 -3
  73. data/config/locales/tr-TR.yml +4 -1
  74. data/config/locales/uk.yml +4 -1
  75. data/db/migrate/20200320105911_index_foreign_keys_in_decidim_comments_comments.rb +7 -0
  76. data/lib/decidim/comments.rb +1 -0
  77. data/lib/decidim/comments/markdown.rb +55 -0
  78. data/lib/decidim/comments/test/shared_examples/comment_event.rb +11 -1
  79. data/lib/decidim/comments/test/shared_examples/create_comment_context.rb +3 -2
  80. data/lib/decidim/comments/version.rb +1 -1
  81. metadata +46 -9
@@ -50,7 +50,7 @@ describe("<Comment />", () => {
50
50
  orderBy={orderBy}
51
51
  />
52
52
  );
53
- expect(wrapper.find("article.comment").exists()).toBeTruthy();
53
+ expect(wrapper.find(".comment").exists()).toBeTruthy();
54
54
  });
55
55
 
56
56
  it("should render a time tag with comment's created at", () => {
@@ -13,13 +13,17 @@ import {
13
13
  CommentFragment
14
14
  } from "../support/schema";
15
15
 
16
+ import { NetworkStatus } from "apollo-client";
17
+
16
18
  const { I18n } = require("react-i18nify");
17
19
 
18
20
  interface CommentProps {
19
21
  comment: CommentFragment;
20
- session: AddCommentFormSessionFragment & {
21
- user: any;
22
- } | null;
22
+ session:
23
+ | AddCommentFormSessionFragment & {
24
+ user: any;
25
+ }
26
+ | null;
23
27
  articleClassName?: string;
24
28
  isRootComment?: boolean;
25
29
  votable?: boolean;
@@ -28,9 +32,14 @@ interface CommentProps {
28
32
  }
29
33
 
30
34
  interface CommentState {
35
+ showReplies: boolean;
31
36
  showReplyForm: boolean;
32
37
  }
33
38
 
39
+ interface Dict {
40
+ [key: string]: boolean | undefined;
41
+ }
42
+
34
43
  /**
35
44
  * A single comment component with the author info and the comment's body
36
45
  * @class
@@ -44,18 +53,26 @@ class Comment extends React.Component<CommentProps, CommentState> {
44
53
  votable: false
45
54
  };
46
55
 
47
- public commentNode: HTMLElement;
56
+ public commentNode: HTMLDivElement;
48
57
 
49
58
  constructor(props: CommentProps) {
50
59
  super(props);
51
60
 
61
+ const {
62
+ comment: { id }
63
+ } = props;
64
+ const isThreadHidden = !!this.getThreadsStorage()[id];
65
+
52
66
  this.state = {
67
+ showReplies: !isThreadHidden,
53
68
  showReplyForm: false
54
69
  };
55
70
  }
56
71
 
57
72
  public componentDidMount() {
58
- const { comment: { id } } = this.props;
73
+ const {
74
+ comment: { id }
75
+ } = this.props;
59
76
  const hash = document.location.hash;
60
77
  const regex = new RegExp(`#comment_${id}`);
61
78
 
@@ -64,14 +81,14 @@ class Comment extends React.Component<CommentProps, CommentState> {
64
81
  return;
65
82
  }
66
83
  const difference = to - element.scrollTop;
67
- const perTick = difference / duration * 10;
84
+ const perTick = (difference / duration) * 10;
68
85
 
69
86
  setTimeout(() => {
70
- element.scrollTop = element.scrollTop + perTick;
71
- if (element.scrollTop === to) {
72
- return;
73
- }
74
- scrollTo(element, to, duration - 10);
87
+ element.scrollTop = element.scrollTop + perTick;
88
+ if (element.scrollTop === to) {
89
+ return;
90
+ }
91
+ scrollTo(element, to, duration - 10);
75
92
  }, 10);
76
93
  }
77
94
 
@@ -84,46 +101,91 @@ class Comment extends React.Component<CommentProps, CommentState> {
84
101
  }
85
102
  }
86
103
 
87
- public getNodeReference = (commentNode: HTMLElement) => this.commentNode = commentNode;
104
+ public getNodeReference = (commentNode: HTMLDivElement) =>
105
+ (this.commentNode = commentNode)
88
106
 
89
107
  public render(): JSX.Element {
90
- const { session, comment: { id, author, formattedBody, createdAt, formattedCreatedAt }, articleClassName } = this.props;
108
+ const {
109
+ session,
110
+ comment: { id, author, formattedBody, createdAt, formattedCreatedAt },
111
+ articleClassName
112
+ } = this.props;
91
113
  let modalName = "loginModal";
92
114
 
93
115
  if (session && session.user) {
94
116
  modalName = `flagModalComment${id}`;
95
117
  }
96
118
 
119
+ let singleCommentUrl = `${window.location.pathname}?commentId=${id}`;
120
+
121
+ if (window.location.search && window.location.search !== "") {
122
+ singleCommentUrl = `${
123
+ window.location.pathname
124
+ }${window.location.search.replace(/commentId=\d*/gi, `commentId=${id}`)}`;
125
+ }
126
+
97
127
  return (
98
- <article id={`comment_${id}`} className={articleClassName} ref={this.getNodeReference}>
128
+ <div
129
+ id={`comment_${id}`}
130
+ className={articleClassName}
131
+ ref={this.getNodeReference}
132
+ >
99
133
  <div className="comment__header">
100
134
  <div className="author-data">
101
135
  <div className="author-data__main">
102
136
  {this._renderAuthorReference()}
103
- <span><time dateTime={createdAt} title={createdAt}>{formattedCreatedAt}</time></span>
137
+ <span>
138
+ <time dateTime={createdAt} title={createdAt}>
139
+ {formattedCreatedAt}
140
+ </time>
141
+ </span>
104
142
  </div>
105
143
  <div className="author-data__extra">
106
- <button type="button" title={I18n.t("components.comment.report.title")} data-open={modalName}>
107
- <Icon name="icon-flag" iconExtraClassName="icon--small" />
144
+ <button
145
+ type="button"
146
+ className="link-alt"
147
+ title={I18n.t("components.comment.report.title")}
148
+ data-open={modalName}
149
+ >
150
+ <Icon
151
+ name="icon-flag"
152
+ iconExtraClassName="icon--small"
153
+ title={I18n.t("components.comment.report.title")}
154
+ role="img"
155
+ />
108
156
  </button>
109
157
  {this._renderFlagModal()}
158
+ <a
159
+ href={singleCommentUrl}
160
+ title={I18n.t("components.comment.single_comment_link_title")}
161
+ >
162
+ <Icon
163
+ name="icon-link-intact"
164
+ iconExtraClassName="icon--small"
165
+ title={I18n.t("components.comment.single_comment_link_title")}
166
+ role="img"
167
+ />
168
+ </a>
110
169
  </div>
111
170
  </div>
112
171
  </div>
113
172
  <div className="comment__content">
114
173
  <div>
115
174
  {this._renderAlignmentBadge()}
116
- <div dangerouslySetInnerHTML={{__html: formattedBody}} />
175
+ <div dangerouslySetInnerHTML={{ __html: formattedBody }} />
117
176
  </div>
118
177
  </div>
119
178
  <div className="comment__footer">
120
- {this._renderReplyButton()}
179
+ <div className="comment__actions">
180
+ {this._renderShowHideThreadButton()}
181
+ {this._renderReplyButton()}
182
+ </div>
121
183
  {this._renderVoteButtons()}
122
184
  </div>
123
185
  {this._renderReplies()}
124
186
  {this._renderAdditionalReplyButton()}
125
187
  {this._renderReplyForm()}
126
- </article>
188
+ </div>
127
189
  );
128
190
  }
129
191
 
@@ -132,13 +194,52 @@ class Comment extends React.Component<CommentProps, CommentState> {
132
194
  this.setState({ showReplyForm: !showReplyForm });
133
195
  }
134
196
 
197
+ private getThreadsStorage = (): Dict => {
198
+ const storage: Dict =
199
+ JSON.parse(localStorage.hiddenCommentThreads || null) || {};
200
+
201
+ return storage;
202
+ }
203
+
204
+ private saveThreadsStorage = (id: string, state: boolean) => {
205
+ const storage = this.getThreadsStorage();
206
+ storage[parseInt(id, 10)] = state;
207
+ localStorage.hiddenCommentThreads = JSON.stringify(storage);
208
+ }
209
+
210
+ private toggleReplies = () => {
211
+ const {
212
+ comment: { id }
213
+ } = this.props;
214
+ const { showReplies } = this.state;
215
+ const newState = !showReplies;
216
+
217
+ this.saveThreadsStorage(id, !newState);
218
+ this.setState({ showReplies: newState });
219
+ }
220
+
221
+ private countReplies = (comment: CommentFragment): number => {
222
+ const { comments } = comment;
223
+
224
+ if (!comments) {
225
+ return 0;
226
+ }
227
+
228
+ return (
229
+ comments.length +
230
+ comments.map(this.countReplies).reduce((a: number, b: number) => a + b, 0)
231
+ );
232
+ }
233
+
135
234
  /**
136
235
  * Render author information as a link to author's profile
137
236
  * @private
138
237
  * @returns {DOMElement} - Render a link with the author information
139
238
  */
140
239
  private _renderAuthorReference() {
141
- const { comment: { author } } = this.props;
240
+ const {
241
+ comment: { author }
242
+ } = this.props;
142
243
 
143
244
  if (author.profilePath === "") {
144
245
  return this._renderAuthor();
@@ -153,7 +254,9 @@ class Comment extends React.Component<CommentProps, CommentState> {
153
254
  * @returns {DOMElement} - Render all the author information
154
255
  */
155
256
  private _renderAuthor() {
156
- const { comment: { author } } = this.props;
257
+ const {
258
+ comment: { author }
259
+ } = this.props;
157
260
 
158
261
  if (author.deleted) {
159
262
  return this._renderDeletedAuthor();
@@ -168,7 +271,9 @@ class Comment extends React.Component<CommentProps, CommentState> {
168
271
  * @returns {DOMElement} - Render all the author information
169
272
  */
170
273
  private _renderDeletedAuthor() {
171
- const { comment: { author } } = this.props;
274
+ const {
275
+ comment: { author }
276
+ } = this.props;
172
277
 
173
278
  return (
174
279
  <div className="author author--inline">
@@ -190,7 +295,9 @@ class Comment extends React.Component<CommentProps, CommentState> {
190
295
  * @returns {DOMElement} - Render all the author information
191
296
  */
192
297
  private _renderActiveAuthor() {
193
- const { comment: { author } } = this.props;
298
+ const {
299
+ comment: { author }
300
+ } = this.props;
194
301
 
195
302
  return (
196
303
  <div className="author author--inline">
@@ -198,11 +305,11 @@ class Comment extends React.Component<CommentProps, CommentState> {
198
305
  <img src={author.avatarUrl} alt="author-avatar" />
199
306
  </span>
200
307
  <span className="author__name">{author.name}</span>
201
- { author.badge === "" ||
308
+ {author.badge === "" || (
202
309
  <span className="author__badge">
203
310
  <Icon name={`icon-${author.badge}`} />
204
311
  </span>
205
- }
312
+ )}
206
313
  <span className="author__nickname">{author.nickname}</span>
207
314
  </div>
208
315
  );
@@ -214,15 +321,21 @@ class Comment extends React.Component<CommentProps, CommentState> {
214
321
  * @returns {Void|DOMElement} - Render the reply button or not if user can reply
215
322
  */
216
323
  private _renderReplyButton() {
217
- const { comment: { acceptsNewComments, userAllowedToComment }, session } = this.props;
324
+ const {
325
+ comment: { id, acceptsNewComments, userAllowedToComment },
326
+ session
327
+ } = this.props;
218
328
 
219
329
  if (session && acceptsNewComments && userAllowedToComment) {
220
330
  return (
221
331
  <button
222
332
  className="comment__reply muted-link"
223
- aria-controls="comment1-reply"
333
+ aria-controls={`comment${id}-reply`}
334
+ data-toggle={`comment${id}-reply`}
224
335
  onClick={this.toggleReplyForm}
225
336
  >
337
+ <Icon name="icon-pencil" iconExtraClassName="icon--small" />
338
+ &nbsp;
226
339
  {I18n.t("components.comment.reply")}
227
340
  </button>
228
341
  );
@@ -237,17 +350,25 @@ class Comment extends React.Component<CommentProps, CommentState> {
237
350
  * @returns {Void|DOMElement} - Render the reply button or not if user can reply
238
351
  */
239
352
  private _renderAdditionalReplyButton() {
240
- const { comment: { acceptsNewComments, hasComments, userAllowedToComment }, session, isRootComment } = this.props;
353
+ const {
354
+ comment: { id, acceptsNewComments, hasComments, userAllowedToComment },
355
+ session,
356
+ isRootComment
357
+ } = this.props;
358
+ const { showReplies } = this.state;
241
359
 
242
360
  if (session && acceptsNewComments && userAllowedToComment) {
243
- if (hasComments && isRootComment) {
361
+ if (hasComments && isRootComment && showReplies) {
244
362
  return (
245
363
  <div className="comment__additionalreply">
246
364
  <button
247
365
  className="comment__reply muted-link"
248
- aria-controls="comment1-reply"
366
+ aria-controls={`comment${id}-reply`}
367
+ data-toggle={`comment${id}-reply`}
249
368
  onClick={this.toggleReplyForm}
250
369
  >
370
+ <Icon name="icon-pencil" iconExtraClassName="icon--small" />
371
+ &nbsp;
251
372
  {I18n.t("components.comment.reply")}
252
373
  </button>
253
374
  </div>
@@ -257,6 +378,40 @@ class Comment extends React.Component<CommentProps, CommentState> {
257
378
  return null;
258
379
  }
259
380
 
381
+ /**
382
+ * Render show/hide thread button if comment is top-level and has children.
383
+ * @private
384
+ * @returns {Void|DOMElement} - Render the reply button or not
385
+ */
386
+ private _renderShowHideThreadButton() {
387
+ const { comment, isRootComment } = this.props;
388
+ const { id, hasComments } = comment;
389
+ const { showReplies } = this.state;
390
+
391
+ if (hasComments && isRootComment) {
392
+ return (
393
+ <button
394
+ className={`comment__reply muted-link ${
395
+ showReplies ? "comment__is-open" : ""
396
+ }`}
397
+ onClick={this.toggleReplies}
398
+ >
399
+ <Icon name="icon-comment-square" iconExtraClassName="icon--small" />
400
+ &nbsp;
401
+ <span className="comment__text-is-closed">
402
+ {I18n.t("components.comment.show_replies", {
403
+ replies_count: this.countReplies(comment)
404
+ })}
405
+ </span>
406
+ <span className="comment__text-is-open">
407
+ {I18n.t("components.comment.hide_replies")}
408
+ </span>
409
+ </button>
410
+ );
411
+ }
412
+ return null;
413
+ }
414
+
260
415
  /**
261
416
  * Render upVote and downVote buttons when the comment is votable
262
417
  * @private
@@ -264,13 +419,25 @@ class Comment extends React.Component<CommentProps, CommentState> {
264
419
  */
265
420
  private _renderVoteButtons() {
266
421
  const { session, comment, votable, rootCommentable, orderBy } = this.props;
267
- const { comment: { userAllowedToComment } } = this.props;
422
+ const {
423
+ comment: { userAllowedToComment }
424
+ } = this.props;
268
425
 
269
426
  if (votable && userAllowedToComment) {
270
427
  return (
271
428
  <div className="comment__votes">
272
- <UpVoteButton session={session} comment={comment} rootCommentable={rootCommentable} orderBy={orderBy} />
273
- <DownVoteButton session={session} comment={comment} rootCommentable={rootCommentable} orderBy={orderBy} />
429
+ <UpVoteButton
430
+ session={session}
431
+ comment={comment}
432
+ rootCommentable={rootCommentable}
433
+ orderBy={orderBy}
434
+ />
435
+ <DownVoteButton
436
+ session={session}
437
+ comment={comment}
438
+ rootCommentable={rootCommentable}
439
+ orderBy={orderBy}
440
+ />
274
441
  </div>
275
442
  );
276
443
  }
@@ -285,6 +452,7 @@ class Comment extends React.Component<CommentProps, CommentState> {
285
452
  */
286
453
  private _renderReplies() {
287
454
  const { comment: { id, hasComments, comments }, session, votable, articleClassName, rootCommentable, orderBy } = this.props;
455
+ const { showReplies } = this.state;
288
456
  let replyArticleClassName = "comment comment--nested";
289
457
 
290
458
  if (articleClassName === "comment comment--nested") {
@@ -293,7 +461,7 @@ class Comment extends React.Component<CommentProps, CommentState> {
293
461
 
294
462
  if (hasComments) {
295
463
  return (
296
- <div>
464
+ <div id={`comment-${id}-replies`} className={showReplies ? "" : "hide"}>
297
465
  {
298
466
  comments.map((reply: CommentFragment) => (
299
467
  <Comment
@@ -322,7 +490,9 @@ class Comment extends React.Component<CommentProps, CommentState> {
322
490
  private _renderReplyForm() {
323
491
  const { session, comment, rootCommentable, orderBy } = this.props;
324
492
  const { showReplyForm } = this.state;
325
- const { comment: { userAllowedToComment } } = this.props;
493
+ const {
494
+ comment: { userAllowedToComment }
495
+ } = this.props;
326
496
 
327
497
  if (session && showReplyForm && userAllowedToComment) {
328
498
  return (
@@ -348,7 +518,9 @@ class Comment extends React.Component<CommentProps, CommentState> {
348
518
  * @returns {Void|DOMElement} - The alignment's badge or not
349
519
  */
350
520
  private _renderAlignmentBadge() {
351
- const { comment: { alignment } } = this.props;
521
+ const {
522
+ comment: { alignment }
523
+ } = this.props;
352
524
  const spanClassName = classnames("label alignment", {
353
525
  success: alignment === 1,
354
526
  alert: alignment === -1
@@ -380,7 +552,10 @@ class Comment extends React.Component<CommentProps, CommentState> {
380
552
  * @return {Void|DOMElement} - The comment's report modal or not.
381
553
  */
382
554
  private _renderFlagModal() {
383
- const { session, comment: { id, sgid, alreadyReported, userAllowedToComment } } = this.props;
555
+ const {
556
+ session,
557
+ comment: { id, sgid, alreadyReported, userAllowedToComment }
558
+ } = this.props;
384
559
  const authenticityToken = this._getAuthenticityToken();
385
560
 
386
561
  const closeModal = () => {
@@ -389,9 +564,15 @@ class Comment extends React.Component<CommentProps, CommentState> {
389
564
 
390
565
  if (session && session.user && userAllowedToComment) {
391
566
  return (
392
- <div className="reveal flag-modal" id={`flagModalComment${id}`} data-reveal={true}>
567
+ <div
568
+ className="reveal flag-modal"
569
+ id={`flagModalComment${id}`}
570
+ data-reveal={true}
571
+ >
393
572
  <div className="reveal__header">
394
- <h3 className="reveal__title">{I18n.t("components.comment.report.title")}</h3>
573
+ <h3 className="reveal__title">
574
+ {I18n.t("components.comment.report.title")}
575
+ </h3>
395
576
  <button
396
577
  className="close-button"
397
578
  aria-label={I18n.t("components.comment.report.close")}
@@ -401,40 +582,72 @@ class Comment extends React.Component<CommentProps, CommentState> {
401
582
  <span aria-hidden="true">&times;</span>
402
583
  </button>
403
584
  </div>
404
- {
405
- (() => {
406
- if (alreadyReported) {
407
- return (
408
- <p key={`already-reported-comment-${id}`}>{I18n.t("components.comment.report.already_reported")}</p>
409
- );
410
- }
411
- return [
412
- <p key={`report-description-comment-${id}`}>{I18n.t("components.comment.report.description")}</p>,
413
- (
414
- <form key={`report-form-comment-${id}`} method="post" action={`/report?sgid=${sgid}`}>
415
- <input type="hidden" name="authenticity_token" value={authenticityToken} />
416
- <label htmlFor={`report_comment_${id}_reason_spam`}>
417
- <input type="radio" value="spam" name="report[reason]" id={`report_comment_${id}_reason_spam`} defaultChecked={true} />
418
- {I18n.t("components.comment.report.reasons.spam")}
419
- </label>
420
- <label htmlFor={`report_comment_${id}_reason_offensive`}>
421
- <input type="radio" value="offensive" name="report[reason]" id={`report_comment_${id}_reason_offensive`} />
422
- {I18n.t("components.comment.report.reasons.offensive")}
423
- </label>
424
- <label htmlFor={`report_comment_${id}_reason_does_not_belong`}>
425
- <input type="radio" value="does_not_belong" name="report[reason]" id={`report_comment_${id}_reason_does_not_belong`} />
426
- {I18n.t("components.comment.report.reasons.does_not_belong", { organization_name: session.user.organizationName })}
427
- </label>
428
- <label htmlFor={`report_comment_${id}_details`}>
429
- {I18n.t("components.comment.report.details")}
430
- <textarea rows={4} name="report[details]" id={`report_comment_${id}_details`} />
431
- </label>
432
- <button type="submit" name="commit" className="button">{I18n.t("components.comment.report.action")}</button>
433
- </form>
434
- )
435
- ];
436
- })()
437
- }
585
+ {(() => {
586
+ if (alreadyReported) {
587
+ return (
588
+ <p key={`already-reported-comment-${id}`}>
589
+ {I18n.t("components.comment.report.already_reported")}
590
+ </p>
591
+ );
592
+ }
593
+ return [
594
+ <p key={`report-description-comment-${id}`}>
595
+ {I18n.t("components.comment.report.description")}
596
+ </p>,
597
+ <form
598
+ key={`report-form-comment-${id}`}
599
+ method="post"
600
+ action={`/report?sgid=${sgid}`}
601
+ >
602
+ <input
603
+ type="hidden"
604
+ name="authenticity_token"
605
+ value={authenticityToken}
606
+ />
607
+ <label htmlFor={`report_comment_${id}_reason_spam`}>
608
+ <input
609
+ type="radio"
610
+ value="spam"
611
+ name="report[reason]"
612
+ id={`report_comment_${id}_reason_spam`}
613
+ defaultChecked={true}
614
+ />
615
+ {I18n.t("components.comment.report.reasons.spam")}
616
+ </label>
617
+ <label htmlFor={`report_comment_${id}_reason_offensive`}>
618
+ <input
619
+ type="radio"
620
+ value="offensive"
621
+ name="report[reason]"
622
+ id={`report_comment_${id}_reason_offensive`}
623
+ />
624
+ {I18n.t("components.comment.report.reasons.offensive")}
625
+ </label>
626
+ <label htmlFor={`report_comment_${id}_reason_does_not_belong`}>
627
+ <input
628
+ type="radio"
629
+ value="does_not_belong"
630
+ name="report[reason]"
631
+ id={`report_comment_${id}_reason_does_not_belong`}
632
+ />
633
+ {I18n.t("components.comment.report.reasons.does_not_belong", {
634
+ organization_name: session.user.organizationName
635
+ })}
636
+ </label>
637
+ <label htmlFor={`report_comment_${id}_details`}>
638
+ {I18n.t("components.comment.report.details")}
639
+ <textarea
640
+ rows={4}
641
+ name="report[details]"
642
+ id={`report_comment_${id}_details`}
643
+ />
644
+ </label>
645
+ <button type="submit" name="commit" className="button">
646
+ {I18n.t("components.comment.report.action")}
647
+ </button>
648
+ </form>
649
+ ];
650
+ })()}
438
651
  </div>
439
652
  );
440
653
  }