]> Untitled Git - lemmy-ui.git/blob - src/shared/components/person/reports.tsx
Adding private message reporting. Fixes #782 (#806)
[lemmy-ui.git] / src / shared / components / person / reports.tsx
1 import { None, Option, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
3 import {
4   CommentReportResponse,
5   CommentReportView,
6   GetSiteResponse,
7   ListCommentReports,
8   ListCommentReportsResponse,
9   ListPostReports,
10   ListPostReportsResponse,
11   ListPrivateMessageReports,
12   ListPrivateMessageReportsResponse,
13   PostReportResponse,
14   PostReportView,
15   PrivateMessageReportResponse,
16   PrivateMessageReportView,
17   UserOperation,
18   wsJsonToRes,
19   wsUserOp,
20 } from "lemmy-js-client";
21 import { Subscription } from "rxjs";
22 import { i18n } from "../../i18next";
23 import { InitialFetchRequest } from "../../interfaces";
24 import { UserService, WebSocketService } from "../../services";
25 import {
26   amAdmin,
27   auth,
28   fetchLimit,
29   isBrowser,
30   setIsoData,
31   setupTippy,
32   toast,
33   updateCommentReportRes,
34   updatePostReportRes,
35   updatePrivateMessageReportRes,
36   wsClient,
37   wsSubscribe,
38 } from "../../utils";
39 import { CommentReport } from "../comment/comment-report";
40 import { HtmlTags } from "../common/html-tags";
41 import { Spinner } from "../common/icon";
42 import { Paginator } from "../common/paginator";
43 import { PostReport } from "../post/post-report";
44 import { PrivateMessageReport } from "../private_message/private-message-report";
45
46 enum UnreadOrAll {
47   Unread,
48   All,
49 }
50
51 enum MessageType {
52   All,
53   CommentReport,
54   PostReport,
55   PrivateMessageReport,
56 }
57
58 enum MessageEnum {
59   CommentReport,
60   PostReport,
61   PrivateMessageReport,
62 }
63
64 type ItemType = {
65   id: number;
66   type_: MessageEnum;
67   view: CommentReportView | PostReportView | PrivateMessageReportView;
68   published: string;
69 };
70
71 interface ReportsState {
72   listCommentReportsResponse: Option<ListCommentReportsResponse>;
73   listPostReportsResponse: Option<ListPostReportsResponse>;
74   listPrivateMessageReportsResponse: Option<ListPrivateMessageReportsResponse>;
75   unreadOrAll: UnreadOrAll;
76   messageType: MessageType;
77   combined: ItemType[];
78   siteRes: GetSiteResponse;
79   page: number;
80   loading: boolean;
81 }
82
83 export class Reports extends Component<any, ReportsState> {
84   private isoData = setIsoData(
85     this.context,
86     ListCommentReportsResponse,
87     ListPostReportsResponse,
88     ListPrivateMessageReportsResponse
89   );
90   private subscription: Subscription;
91   private emptyState: ReportsState = {
92     listCommentReportsResponse: None,
93     listPostReportsResponse: None,
94     listPrivateMessageReportsResponse: None,
95     unreadOrAll: UnreadOrAll.Unread,
96     messageType: MessageType.All,
97     combined: [],
98     page: 1,
99     siteRes: this.isoData.site_res,
100     loading: true,
101   };
102
103   constructor(props: any, context: any) {
104     super(props, context);
105
106     this.state = this.emptyState;
107     this.handlePageChange = this.handlePageChange.bind(this);
108
109     if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
110       toast(i18n.t("not_logged_in"), "danger");
111       this.context.router.history.push(`/login`);
112     }
113
114     this.parseMessage = this.parseMessage.bind(this);
115     this.subscription = wsSubscribe(this.parseMessage);
116
117     // Only fetch the data if coming from another route
118     if (this.isoData.path == this.context.router.route.match.url) {
119       this.state = {
120         ...this.state,
121         listCommentReportsResponse: Some(
122           this.isoData.routeData[0] as ListCommentReportsResponse
123         ),
124         listPostReportsResponse: Some(
125           this.isoData.routeData[1] as ListPostReportsResponse
126         ),
127       };
128       if (amAdmin()) {
129         this.state = {
130           ...this.state,
131           listPrivateMessageReportsResponse: Some(
132             this.isoData.routeData[2] as ListPrivateMessageReportsResponse
133           ),
134         };
135       }
136       this.state = {
137         ...this.state,
138         combined: this.buildCombined(),
139         loading: false,
140       };
141     } else {
142       this.refetch();
143     }
144   }
145
146   componentWillUnmount() {
147     if (isBrowser()) {
148       this.subscription.unsubscribe();
149     }
150   }
151
152   get documentTitle(): string {
153     return this.state.siteRes.site_view.match({
154       some: siteView =>
155         UserService.Instance.myUserInfo.match({
156           some: mui =>
157             `@${mui.local_user_view.person.name} ${i18n.t("reports")} - ${
158               siteView.site.name
159             }`,
160           none: "",
161         }),
162       none: "",
163     });
164   }
165
166   render() {
167     return (
168       <div className="container">
169         {this.state.loading ? (
170           <h5>
171             <Spinner large />
172           </h5>
173         ) : (
174           <div className="row">
175             <div className="col-12">
176               <HtmlTags
177                 title={this.documentTitle}
178                 path={this.context.router.route.match.url}
179                 description={None}
180                 image={None}
181               />
182               <h5 className="mb-2">{i18n.t("reports")}</h5>
183               {this.selects()}
184               {this.state.messageType == MessageType.All && this.all()}
185               {this.state.messageType == MessageType.CommentReport &&
186                 this.commentReports()}
187               {this.state.messageType == MessageType.PostReport &&
188                 this.postReports()}
189               {this.state.messageType == MessageType.PrivateMessageReport &&
190                 this.privateMessageReports()}
191               <Paginator
192                 page={this.state.page}
193                 onChange={this.handlePageChange}
194               />
195             </div>
196           </div>
197         )}
198       </div>
199     );
200   }
201
202   unreadOrAllRadios() {
203     return (
204       <div className="btn-group btn-group-toggle flex-wrap mb-2">
205         <label
206           className={`btn btn-outline-secondary pointer
207             ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
208           `}
209         >
210           <input
211             type="radio"
212             value={UnreadOrAll.Unread}
213             checked={this.state.unreadOrAll == UnreadOrAll.Unread}
214             onChange={linkEvent(this, this.handleUnreadOrAllChange)}
215           />
216           {i18n.t("unread")}
217         </label>
218         <label
219           className={`btn btn-outline-secondary pointer
220             ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
221           `}
222         >
223           <input
224             type="radio"
225             value={UnreadOrAll.All}
226             checked={this.state.unreadOrAll == UnreadOrAll.All}
227             onChange={linkEvent(this, this.handleUnreadOrAllChange)}
228           />
229           {i18n.t("all")}
230         </label>
231       </div>
232     );
233   }
234
235   messageTypeRadios() {
236     return (
237       <div className="btn-group btn-group-toggle flex-wrap mb-2">
238         <label
239           className={`btn btn-outline-secondary pointer
240             ${this.state.messageType == MessageType.All && "active"}
241           `}
242         >
243           <input
244             type="radio"
245             value={MessageType.All}
246             checked={this.state.messageType == MessageType.All}
247             onChange={linkEvent(this, this.handleMessageTypeChange)}
248           />
249           {i18n.t("all")}
250         </label>
251         <label
252           className={`btn btn-outline-secondary pointer
253             ${this.state.messageType == MessageType.CommentReport && "active"}
254           `}
255         >
256           <input
257             type="radio"
258             value={MessageType.CommentReport}
259             checked={this.state.messageType == MessageType.CommentReport}
260             onChange={linkEvent(this, this.handleMessageTypeChange)}
261           />
262           {i18n.t("comments")}
263         </label>
264         <label
265           className={`btn btn-outline-secondary pointer
266             ${this.state.messageType == MessageType.PostReport && "active"}
267           `}
268         >
269           <input
270             type="radio"
271             value={MessageType.PostReport}
272             checked={this.state.messageType == MessageType.PostReport}
273             onChange={linkEvent(this, this.handleMessageTypeChange)}
274           />
275           {i18n.t("posts")}
276         </label>
277         {amAdmin() && (
278           <label
279             className={`btn btn-outline-secondary pointer
280             ${
281               this.state.messageType == MessageType.PrivateMessageReport &&
282               "active"
283             }
284           `}
285           >
286             <input
287               type="radio"
288               value={MessageType.PrivateMessageReport}
289               checked={
290                 this.state.messageType == MessageType.PrivateMessageReport
291               }
292               onChange={linkEvent(this, this.handleMessageTypeChange)}
293             />
294             {i18n.t("messages")}
295           </label>
296         )}
297       </div>
298     );
299   }
300
301   selects() {
302     return (
303       <div className="mb-2">
304         <span className="mr-3">{this.unreadOrAllRadios()}</span>
305         <span className="mr-3">{this.messageTypeRadios()}</span>
306       </div>
307     );
308   }
309
310   commentReportToItemType(r: CommentReportView): ItemType {
311     return {
312       id: r.comment_report.id,
313       type_: MessageEnum.CommentReport,
314       view: r,
315       published: r.comment_report.published,
316     };
317   }
318
319   postReportToItemType(r: PostReportView): ItemType {
320     return {
321       id: r.post_report.id,
322       type_: MessageEnum.PostReport,
323       view: r,
324       published: r.post_report.published,
325     };
326   }
327
328   privateMessageReportToItemType(r: PrivateMessageReportView): ItemType {
329     return {
330       id: r.private_message_report.id,
331       type_: MessageEnum.PrivateMessageReport,
332       view: r,
333       published: r.private_message_report.published,
334     };
335   }
336
337   buildCombined(): ItemType[] {
338     let comments: ItemType[] = this.state.listCommentReportsResponse
339       .map(r => r.comment_reports)
340       .unwrapOr([])
341       .map(r => this.commentReportToItemType(r));
342     let posts: ItemType[] = this.state.listPostReportsResponse
343       .map(r => r.post_reports)
344       .unwrapOr([])
345       .map(r => this.postReportToItemType(r));
346     let privateMessages: ItemType[] =
347       this.state.listPrivateMessageReportsResponse
348         .map(r => r.private_message_reports)
349         .unwrapOr([])
350         .map(r => this.privateMessageReportToItemType(r));
351
352     return [...comments, ...posts, ...privateMessages].sort((a, b) =>
353       b.published.localeCompare(a.published)
354     );
355   }
356
357   renderItemType(i: ItemType) {
358     switch (i.type_) {
359       case MessageEnum.CommentReport:
360         return (
361           <CommentReport key={i.id} report={i.view as CommentReportView} />
362         );
363       case MessageEnum.PostReport:
364         return <PostReport key={i.id} report={i.view as PostReportView} />;
365       case MessageEnum.PrivateMessageReport:
366         return (
367           <PrivateMessageReport
368             key={i.id}
369             report={i.view as PrivateMessageReportView}
370           />
371         );
372       default:
373         return <div />;
374     }
375   }
376
377   all() {
378     return (
379       <div>
380         {this.state.combined.map(i => (
381           <>
382             <hr />
383             {this.renderItemType(i)}
384           </>
385         ))}
386       </div>
387     );
388   }
389
390   commentReports() {
391     return this.state.listCommentReportsResponse.match({
392       some: res => (
393         <div>
394           {res.comment_reports.map(cr => (
395             <>
396               <hr />
397               <CommentReport key={cr.comment_report.id} report={cr} />
398             </>
399           ))}
400         </div>
401       ),
402       none: <></>,
403     });
404   }
405
406   postReports() {
407     return this.state.listPostReportsResponse.match({
408       some: res => (
409         <div>
410           {res.post_reports.map(pr => (
411             <>
412               <hr />
413               <PostReport key={pr.post_report.id} report={pr} />
414             </>
415           ))}
416         </div>
417       ),
418       none: <></>,
419     });
420   }
421
422   privateMessageReports() {
423     return this.state.listPrivateMessageReportsResponse.match({
424       some: res => (
425         <div>
426           {res.private_message_reports.map(pmr => (
427             <>
428               <hr />
429               <PrivateMessageReport
430                 key={pmr.private_message_report.id}
431                 report={pmr}
432               />
433             </>
434           ))}
435         </div>
436       ),
437       none: <></>,
438     });
439   }
440
441   handlePageChange(page: number) {
442     this.setState({ page });
443     this.refetch();
444   }
445
446   handleUnreadOrAllChange(i: Reports, event: any) {
447     i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
448     i.refetch();
449   }
450
451   handleMessageTypeChange(i: Reports, event: any) {
452     i.setState({ messageType: Number(event.target.value), page: 1 });
453     i.refetch();
454   }
455
456   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
457     let promises: Promise<any>[] = [];
458
459     let unresolved_only = Some(true);
460     let page = Some(1);
461     let limit = Some(fetchLimit);
462     let community_id = None;
463     let auth = req.auth.unwrap();
464
465     let commentReportsForm = new ListCommentReports({
466       // TODO community_id
467       unresolved_only,
468       community_id,
469       page,
470       limit,
471       auth,
472     });
473     promises.push(req.client.listCommentReports(commentReportsForm));
474
475     let postReportsForm = new ListPostReports({
476       // TODO community_id
477       unresolved_only,
478       community_id,
479       page,
480       limit,
481       auth,
482     });
483     promises.push(req.client.listPostReports(postReportsForm));
484
485     if (amAdmin()) {
486       let privateMessageReportsForm = new ListPrivateMessageReports({
487         unresolved_only,
488         page,
489         limit,
490         auth,
491       });
492       promises.push(
493         req.client.listPrivateMessageReports(privateMessageReportsForm)
494       );
495     }
496
497     return promises;
498   }
499
500   refetch() {
501     let unresolved_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
502     let community_id = None;
503     let page = Some(this.state.page);
504     let limit = Some(fetchLimit);
505
506     let commentReportsForm = new ListCommentReports({
507       unresolved_only,
508       // TODO community_id
509       community_id,
510       page,
511       limit,
512       auth: auth().unwrap(),
513     });
514     WebSocketService.Instance.send(
515       wsClient.listCommentReports(commentReportsForm)
516     );
517
518     let postReportsForm = new ListPostReports({
519       unresolved_only,
520       // TODO community_id
521       community_id,
522       page,
523       limit,
524       auth: auth().unwrap(),
525     });
526     WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm));
527
528     if (amAdmin()) {
529       let privateMessageReportsForm = new ListPrivateMessageReports({
530         unresolved_only,
531         page,
532         limit,
533         auth: auth().unwrap(),
534       });
535       WebSocketService.Instance.send(
536         wsClient.listPrivateMessageReports(privateMessageReportsForm)
537       );
538     }
539   }
540
541   parseMessage(msg: any) {
542     let op = wsUserOp(msg);
543     console.log(msg);
544     if (msg.error) {
545       toast(i18n.t(msg.error), "danger");
546       return;
547     } else if (msg.reconnect) {
548       this.refetch();
549     } else if (op == UserOperation.ListCommentReports) {
550       let data = wsJsonToRes<ListCommentReportsResponse>(
551         msg,
552         ListCommentReportsResponse
553       );
554       this.setState({ listCommentReportsResponse: Some(data) });
555       this.setState({ combined: this.buildCombined(), loading: false });
556       // this.sendUnreadCount();
557       window.scrollTo(0, 0);
558       setupTippy();
559     } else if (op == UserOperation.ListPostReports) {
560       let data = wsJsonToRes<ListPostReportsResponse>(
561         msg,
562         ListPostReportsResponse
563       );
564       this.setState({ listPostReportsResponse: Some(data) });
565       this.setState({ combined: this.buildCombined(), loading: false });
566       // this.sendUnreadCount();
567       window.scrollTo(0, 0);
568       setupTippy();
569     } else if (op == UserOperation.ListPrivateMessageReports) {
570       let data = wsJsonToRes<ListPrivateMessageReportsResponse>(
571         msg,
572         ListPrivateMessageReportsResponse
573       );
574       this.setState({ listPrivateMessageReportsResponse: Some(data) });
575       this.setState({ combined: this.buildCombined(), loading: false });
576       // this.sendUnreadCount();
577       window.scrollTo(0, 0);
578       setupTippy();
579     } else if (op == UserOperation.ResolvePostReport) {
580       let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
581       updatePostReportRes(
582         data.post_report_view,
583         this.state.listPostReportsResponse.map(r => r.post_reports).unwrapOr([])
584       );
585       let urcs = UserService.Instance.unreadReportCountSub;
586       if (data.post_report_view.post_report.resolved) {
587         urcs.next(urcs.getValue() - 1);
588       } else {
589         urcs.next(urcs.getValue() + 1);
590       }
591       this.setState(this.state);
592     } else if (op == UserOperation.ResolveCommentReport) {
593       let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
594       updateCommentReportRes(
595         data.comment_report_view,
596         this.state.listCommentReportsResponse
597           .map(r => r.comment_reports)
598           .unwrapOr([])
599       );
600       let urcs = UserService.Instance.unreadReportCountSub;
601       if (data.comment_report_view.comment_report.resolved) {
602         urcs.next(urcs.getValue() - 1);
603       } else {
604         urcs.next(urcs.getValue() + 1);
605       }
606       this.setState(this.state);
607     } else if (op == UserOperation.ResolvePrivateMessageReport) {
608       let data = wsJsonToRes<PrivateMessageReportResponse>(
609         msg,
610         PrivateMessageReportResponse
611       );
612       updatePrivateMessageReportRes(
613         data.private_message_report_view,
614         this.state.listPrivateMessageReportsResponse
615           .map(r => r.private_message_reports)
616           .unwrapOr([])
617       );
618       let urcs = UserService.Instance.unreadReportCountSub;
619       if (data.private_message_report_view.private_message_report.resolved) {
620         urcs.next(urcs.getValue() - 1);
621       } else {
622         urcs.next(urcs.getValue() + 1);
623       }
624       this.setState(this.state);
625     }
626   }
627 }