decidim-comments 0.19.0 → 0.22.0

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/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
  }