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