]> Untitled Git - lemmy-ui.git/blob - src/shared/components/post/post.tsx
Adding purging of comments, posts, communities, and users. (#459)
[lemmy-ui.git] / src / shared / components / post / post.tsx
1 import { None, Option, Right, Some } from "@sniptt/monads";
2 import autosize from "autosize";
3 import { Component, createRef, linkEvent, RefObject } from "inferno";
4 import {
5   AddAdminResponse,
6   AddModToCommunityResponse,
7   BanFromCommunityResponse,
8   BanPersonResponse,
9   BlockPersonResponse,
10   CommentReportResponse,
11   CommentResponse,
12   CommunityResponse,
13   GetCommunityResponse,
14   GetPost,
15   GetPostResponse,
16   GetSiteResponse,
17   ListingType,
18   MarkCommentAsRead,
19   PostReportResponse,
20   PostResponse,
21   PostView,
22   PurgeItemResponse,
23   Search,
24   SearchResponse,
25   SearchType,
26   SortType,
27   UserOperation,
28   wsJsonToRes,
29   wsUserOp,
30 } from "lemmy-js-client";
31 import { Subscription } from "rxjs";
32 import { i18n } from "../../i18next";
33 import {
34   CommentNode as CommentNodeI,
35   CommentSortType,
36   CommentViewType,
37   InitialFetchRequest,
38 } from "../../interfaces";
39 import { UserService, WebSocketService } from "../../services";
40 import {
41   auth,
42   buildCommentsTree,
43   commentsToFlatNodes,
44   createCommentLikeRes,
45   createPostLikeRes,
46   debounce,
47   editCommentRes,
48   enableDownvotes,
49   enableNsfw,
50   getCommentIdFromProps,
51   getIdFromProps,
52   insertCommentIntoTree,
53   isBrowser,
54   isImage,
55   restoreScrollPosition,
56   saveCommentRes,
57   saveScrollPosition,
58   setIsoData,
59   setupTippy,
60   toast,
61   trendingFetchLimit,
62   updatePersonBlock,
63   wsClient,
64   wsSubscribe,
65 } from "../../utils";
66 import { CommentForm } from "../comment/comment-form";
67 import { CommentNodes } from "../comment/comment-nodes";
68 import { HtmlTags } from "../common/html-tags";
69 import { Icon, Spinner } from "../common/icon";
70 import { Sidebar } from "../community/sidebar";
71 import { PostListing } from "./post-listing";
72
73 const commentsShownInterval = 15;
74
75 interface PostState {
76   postRes: Option<GetPostResponse>;
77   postId: number;
78   commentTree: CommentNodeI[];
79   commentId?: number;
80   commentSort: CommentSortType;
81   commentViewType: CommentViewType;
82   scrolled?: boolean;
83   loading: boolean;
84   crossPosts: Option<PostView[]>;
85   siteRes: GetSiteResponse;
86   commentSectionRef?: RefObject<HTMLDivElement>;
87   showSidebarMobile: boolean;
88   maxCommentsShown: number;
89 }
90
91 export class Post extends Component<any, PostState> {
92   private subscription: Subscription;
93   private isoData = setIsoData(this.context, GetPostResponse);
94   private commentScrollDebounced: () => void;
95   private emptyState: PostState = {
96     postRes: None,
97     postId: getIdFromProps(this.props),
98     commentTree: [],
99     commentId: getCommentIdFromProps(this.props),
100     commentSort: CommentSortType.Hot,
101     commentViewType: CommentViewType.Tree,
102     scrolled: false,
103     loading: true,
104     crossPosts: None,
105     siteRes: this.isoData.site_res,
106     commentSectionRef: null,
107     showSidebarMobile: false,
108     maxCommentsShown: commentsShownInterval,
109   };
110
111   constructor(props: any, context: any) {
112     super(props, context);
113
114     this.state = this.emptyState;
115     this.state.commentSectionRef = createRef();
116
117     this.parseMessage = this.parseMessage.bind(this);
118     this.subscription = wsSubscribe(this.parseMessage);
119
120     // Only fetch the data if coming from another route
121     if (this.isoData.path == this.context.router.route.match.url) {
122       this.state.postRes = Some(this.isoData.routeData[0] as GetPostResponse);
123       this.state.commentTree = buildCommentsTree(
124         this.state.postRes.unwrap().comments,
125         this.state.commentSort
126       );
127       this.state.loading = false;
128
129       if (isBrowser()) {
130         WebSocketService.Instance.send(
131           wsClient.communityJoin({
132             community_id:
133               this.state.postRes.unwrap().community_view.community.id,
134           })
135         );
136         WebSocketService.Instance.send(
137           wsClient.postJoin({ post_id: this.state.postId })
138         );
139
140         this.fetchCrossPosts();
141         if (this.state.commentId) {
142           this.scrollCommentIntoView();
143         }
144
145         if (this.checkScrollIntoCommentsParam) {
146           this.scrollIntoCommentSection();
147         }
148       }
149     } else {
150       this.fetchPost();
151     }
152   }
153
154   fetchPost() {
155     let form = new GetPost({
156       id: this.state.postId,
157       auth: auth(false).ok(),
158     });
159     WebSocketService.Instance.send(wsClient.getPost(form));
160   }
161
162   fetchCrossPosts() {
163     this.state.postRes
164       .andThen(r => r.post_view.post.url)
165       .match({
166         some: url => {
167           let form = new Search({
168             q: url,
169             type_: Some(SearchType.Url),
170             sort: Some(SortType.TopAll),
171             listing_type: Some(ListingType.All),
172             page: Some(1),
173             limit: Some(trendingFetchLimit),
174             community_id: None,
175             community_name: None,
176             creator_id: None,
177             auth: auth(false).ok(),
178           });
179           WebSocketService.Instance.send(wsClient.search(form));
180         },
181         none: void 0,
182       });
183   }
184
185   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
186     let pathSplit = req.path.split("/");
187
188     let id = Number(pathSplit[2]);
189
190     let postForm = new GetPost({
191       id,
192       auth: req.auth,
193     });
194
195     return [req.client.getPost(postForm)];
196   }
197
198   componentWillUnmount() {
199     this.subscription.unsubscribe();
200     document.removeEventListener("scroll", this.commentScrollDebounced);
201
202     saveScrollPosition(this.context);
203   }
204
205   componentDidMount() {
206     autosize(document.querySelectorAll("textarea"));
207
208     this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100);
209     document.addEventListener("scroll", this.commentScrollDebounced);
210   }
211
212   componentDidUpdate(_lastProps: any) {
213     // Necessary if you are on a post and you click another post (same route)
214     if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
215       // TODO Couldnt get a refresh working. This does for now.
216       location.reload();
217
218       // let currentId = this.props.match.params.id;
219       // WebSocketService.Instance.getPost(currentId);
220       // this.context.refresh();
221       // this.context.router.history.push(_lastProps.location.pathname);
222     }
223   }
224
225   scrollCommentIntoView() {
226     let commentElement = document.getElementById(
227       `comment-${this.state.commentId}`
228     );
229     if (commentElement) {
230       commentElement.scrollIntoView();
231       commentElement.classList.add("mark");
232       this.state.scrolled = true;
233       this.markScrolledAsRead(this.state.commentId);
234     }
235   }
236
237   get checkScrollIntoCommentsParam() {
238     return Boolean(
239       new URLSearchParams(this.props.location.search).get("scrollToComments")
240     );
241   }
242
243   scrollIntoCommentSection() {
244     this.state.commentSectionRef.current?.scrollIntoView();
245   }
246
247   // TODO this needs some re-work
248   markScrolledAsRead(commentId: number) {
249     this.state.postRes.match({
250       some: res => {
251         let found = res.comments.find(c => c.comment.id == commentId);
252         let parent = res.comments.find(
253           c => found.comment.parent_id.unwrapOr(0) == c.comment.id
254         );
255         let parent_person_id = parent
256           ? parent.creator.id
257           : res.post_view.creator.id;
258
259         UserService.Instance.myUserInfo.match({
260           some: mui => {
261             if (mui.local_user_view.person.id == parent_person_id) {
262               let form = new MarkCommentAsRead({
263                 comment_id: found.comment.id,
264                 read: true,
265                 auth: auth().unwrap(),
266               });
267               WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
268               UserService.Instance.unreadInboxCountSub.next(
269                 UserService.Instance.unreadInboxCountSub.value - 1
270               );
271             }
272           },
273           none: void 0,
274         });
275       },
276       none: void 0,
277     });
278   }
279
280   isBottom(el: Element): boolean {
281     return el?.getBoundingClientRect().bottom <= window.innerHeight;
282   }
283
284   /**
285    * Shows new comments when scrolling to the bottom of the comments div
286    */
287   trackCommentsBoxScrolling = () => {
288     const wrappedElement = document.getElementsByClassName("comments")[0];
289     if (wrappedElement && this.isBottom(wrappedElement)) {
290       this.state.maxCommentsShown += commentsShownInterval;
291       this.setState(this.state);
292     }
293   };
294
295   get documentTitle(): string {
296     return this.state.postRes.match({
297       some: res =>
298         this.state.siteRes.site_view.match({
299           some: siteView =>
300             `${res.post_view.post.name} - ${siteView.site.name}`,
301           none: "",
302         }),
303       none: "",
304     });
305   }
306
307   get imageTag(): Option<string> {
308     return this.state.postRes.match({
309       some: res =>
310         res.post_view.post.thumbnail_url.or(
311           res.post_view.post.url.match({
312             some: url => (isImage(url) ? Some(url) : None),
313             none: None,
314           })
315         ),
316       none: None,
317     });
318   }
319
320   get descriptionTag(): Option<string> {
321     return this.state.postRes.andThen(r => r.post_view.post.body);
322   }
323
324   render() {
325     return (
326       <div class="container">
327         {this.state.loading ? (
328           <h5>
329             <Spinner large />
330           </h5>
331         ) : (
332           this.state.postRes.match({
333             some: res => (
334               <div class="row">
335                 <div class="col-12 col-md-8 mb-3">
336                   <HtmlTags
337                     title={this.documentTitle}
338                     path={this.context.router.route.match.url}
339                     image={this.imageTag}
340                     description={this.descriptionTag}
341                   />
342                   <PostListing
343                     post_view={res.post_view}
344                     duplicates={this.state.crossPosts}
345                     showBody
346                     showCommunity
347                     moderators={Some(res.moderators)}
348                     admins={Some(this.state.siteRes.admins)}
349                     enableDownvotes={enableDownvotes(this.state.siteRes)}
350                     enableNsfw={enableNsfw(this.state.siteRes)}
351                   />
352                   <div ref={this.state.commentSectionRef} className="mb-2" />
353                   <CommentForm
354                     node={Right(this.state.postId)}
355                     disabled={res.post_view.post.locked}
356                   />
357                   <div class="d-block d-md-none">
358                     <button
359                       class="btn btn-secondary d-inline-block mb-2 mr-3"
360                       onClick={linkEvent(this, this.handleShowSidebarMobile)}
361                     >
362                       {i18n.t("sidebar")}{" "}
363                       <Icon
364                         icon={
365                           this.state.showSidebarMobile
366                             ? `minus-square`
367                             : `plus-square`
368                         }
369                         classes="icon-inline"
370                       />
371                     </button>
372                     {this.state.showSidebarMobile && this.sidebar()}
373                   </div>
374                   {res.comments.length > 0 && this.sortRadios()}
375                   {this.state.commentViewType == CommentViewType.Tree &&
376                     this.commentsTree()}
377                   {this.state.commentViewType == CommentViewType.Chat &&
378                     this.commentsFlat()}
379                 </div>
380                 <div class="d-none d-md-block col-md-4">{this.sidebar()}</div>
381               </div>
382             ),
383             none: <></>,
384           })
385         )}
386       </div>
387     );
388   }
389
390   sortRadios() {
391     return (
392       <>
393         <div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
394           <label
395             className={`btn btn-outline-secondary pointer ${
396               this.state.commentSort === CommentSortType.Hot && "active"
397             }`}
398           >
399             {i18n.t("hot")}
400             <input
401               type="radio"
402               value={CommentSortType.Hot}
403               checked={this.state.commentSort === CommentSortType.Hot}
404               onChange={linkEvent(this, this.handleCommentSortChange)}
405             />
406           </label>
407           <label
408             className={`btn btn-outline-secondary pointer ${
409               this.state.commentSort === CommentSortType.Top && "active"
410             }`}
411           >
412             {i18n.t("top")}
413             <input
414               type="radio"
415               value={CommentSortType.Top}
416               checked={this.state.commentSort === CommentSortType.Top}
417               onChange={linkEvent(this, this.handleCommentSortChange)}
418             />
419           </label>
420           <label
421             className={`btn btn-outline-secondary pointer ${
422               this.state.commentSort === CommentSortType.New && "active"
423             }`}
424           >
425             {i18n.t("new")}
426             <input
427               type="radio"
428               value={CommentSortType.New}
429               checked={this.state.commentSort === CommentSortType.New}
430               onChange={linkEvent(this, this.handleCommentSortChange)}
431             />
432           </label>
433           <label
434             className={`btn btn-outline-secondary pointer ${
435               this.state.commentSort === CommentSortType.Old && "active"
436             }`}
437           >
438             {i18n.t("old")}
439             <input
440               type="radio"
441               value={CommentSortType.Old}
442               checked={this.state.commentSort === CommentSortType.Old}
443               onChange={linkEvent(this, this.handleCommentSortChange)}
444             />
445           </label>
446         </div>
447         <div class="btn-group btn-group-toggle flex-wrap mb-2">
448           <label
449             className={`btn btn-outline-secondary pointer ${
450               this.state.commentViewType === CommentViewType.Chat && "active"
451             }`}
452           >
453             {i18n.t("chat")}
454             <input
455               type="radio"
456               value={CommentViewType.Chat}
457               checked={this.state.commentViewType === CommentViewType.Chat}
458               onChange={linkEvent(this, this.handleCommentViewTypeChange)}
459             />
460           </label>
461         </div>
462       </>
463     );
464   }
465
466   commentsFlat() {
467     // These are already sorted by new
468     return this.state.postRes.match({
469       some: res => (
470         <div>
471           <CommentNodes
472             nodes={commentsToFlatNodes(res.comments)}
473             maxCommentsShown={Some(this.state.maxCommentsShown)}
474             noIndent
475             locked={res.post_view.post.locked}
476             moderators={Some(res.moderators)}
477             admins={Some(this.state.siteRes.admins)}
478             enableDownvotes={enableDownvotes(this.state.siteRes)}
479             showContext
480           />
481         </div>
482       ),
483       none: <></>,
484     });
485   }
486
487   sidebar() {
488     return this.state.postRes.match({
489       some: res => (
490         <div class="mb-3">
491           <Sidebar
492             community_view={res.community_view}
493             moderators={res.moderators}
494             admins={this.state.siteRes.admins}
495             online={res.online}
496             enableNsfw={enableNsfw(this.state.siteRes)}
497             showIcon
498           />
499         </div>
500       ),
501       none: <></>,
502     });
503   }
504
505   handleCommentSortChange(i: Post, event: any) {
506     i.state.commentSort = Number(event.target.value);
507     i.state.commentViewType = CommentViewType.Tree;
508     i.state.commentTree = buildCommentsTree(
509       i.state.postRes.map(r => r.comments).unwrapOr([]),
510       i.state.commentSort
511     );
512     i.setState(i.state);
513   }
514
515   handleCommentViewTypeChange(i: Post, event: any) {
516     i.state.commentViewType = Number(event.target.value);
517     i.state.commentSort = CommentSortType.New;
518     i.state.commentTree = buildCommentsTree(
519       i.state.postRes.map(r => r.comments).unwrapOr([]),
520       i.state.commentSort
521     );
522     i.setState(i.state);
523   }
524
525   handleShowSidebarMobile(i: Post) {
526     i.state.showSidebarMobile = !i.state.showSidebarMobile;
527     i.setState(i.state);
528   }
529
530   commentsTree() {
531     return this.state.postRes.match({
532       some: res => (
533         <div>
534           <CommentNodes
535             nodes={this.state.commentTree}
536             maxCommentsShown={Some(this.state.maxCommentsShown)}
537             locked={res.post_view.post.locked}
538             moderators={Some(res.moderators)}
539             admins={Some(this.state.siteRes.admins)}
540             enableDownvotes={enableDownvotes(this.state.siteRes)}
541           />
542         </div>
543       ),
544       none: <></>,
545     });
546   }
547
548   parseMessage(msg: any) {
549     let op = wsUserOp(msg);
550     console.log(msg);
551     if (msg.error) {
552       toast(i18n.t(msg.error), "danger");
553       return;
554     } else if (msg.reconnect) {
555       let postId = Number(this.props.match.params.id);
556       WebSocketService.Instance.send(wsClient.postJoin({ post_id: postId }));
557       WebSocketService.Instance.send(
558         wsClient.getPost({
559           id: postId,
560           auth: auth(false).ok(),
561         })
562       );
563     } else if (op == UserOperation.GetPost) {
564       let data = wsJsonToRes<GetPostResponse>(msg, GetPostResponse);
565       this.state.postRes = Some(data);
566
567       this.state.commentTree = buildCommentsTree(
568         this.state.postRes.map(r => r.comments).unwrapOr([]),
569         this.state.commentSort
570       );
571       this.state.loading = false;
572
573       // join the rooms
574       WebSocketService.Instance.send(
575         wsClient.postJoin({ post_id: this.state.postId })
576       );
577       WebSocketService.Instance.send(
578         wsClient.communityJoin({
579           community_id: data.community_view.community.id,
580         })
581       );
582
583       // Get cross-posts
584       this.fetchCrossPosts();
585       this.setState(this.state);
586       setupTippy();
587       if (!this.state.commentId) restoreScrollPosition(this.context);
588
589       if (this.checkScrollIntoCommentsParam) {
590         this.scrollIntoCommentSection();
591       }
592
593       if (this.state.commentId && !this.state.scrolled) {
594         this.scrollCommentIntoView();
595       }
596     } else if (op == UserOperation.CreateComment) {
597       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
598
599       // Don't get comments from the post room, if the creator is blocked
600       let creatorBlocked = UserService.Instance.myUserInfo
601         .map(m => m.person_blocks)
602         .unwrapOr([])
603         .map(pb => pb.target.id)
604         .includes(data.comment_view.creator.id);
605
606       // Necessary since it might be a user reply, which has the recipients, to avoid double
607       if (data.recipient_ids.length == 0 && !creatorBlocked) {
608         this.state.postRes.match({
609           some: res => {
610             res.comments.unshift(data.comment_view);
611             insertCommentIntoTree(this.state.commentTree, data.comment_view);
612             res.post_view.counts.comments++;
613           },
614           none: void 0,
615         });
616         this.setState(this.state);
617         setupTippy();
618       }
619     } else if (
620       op == UserOperation.EditComment ||
621       op == UserOperation.DeleteComment ||
622       op == UserOperation.RemoveComment
623     ) {
624       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
625       editCommentRes(
626         data.comment_view,
627         this.state.postRes.map(r => r.comments).unwrapOr([])
628       );
629       this.setState(this.state);
630     } else if (op == UserOperation.SaveComment) {
631       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
632       saveCommentRes(
633         data.comment_view,
634         this.state.postRes.map(r => r.comments).unwrapOr([])
635       );
636       this.setState(this.state);
637       setupTippy();
638     } else if (op == UserOperation.CreateCommentLike) {
639       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
640       createCommentLikeRes(
641         data.comment_view,
642         this.state.postRes.map(r => r.comments).unwrapOr([])
643       );
644       this.setState(this.state);
645     } else if (op == UserOperation.CreatePostLike) {
646       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
647       this.state.postRes.match({
648         some: res => createPostLikeRes(data.post_view, res.post_view),
649         none: void 0,
650       });
651       this.setState(this.state);
652     } else if (
653       op == UserOperation.EditPost ||
654       op == UserOperation.DeletePost ||
655       op == UserOperation.RemovePost ||
656       op == UserOperation.LockPost ||
657       op == UserOperation.StickyPost ||
658       op == UserOperation.SavePost
659     ) {
660       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
661       this.state.postRes.match({
662         some: res => (res.post_view = data.post_view),
663         none: void 0,
664       });
665       this.setState(this.state);
666       setupTippy();
667     } else if (
668       op == UserOperation.EditCommunity ||
669       op == UserOperation.DeleteCommunity ||
670       op == UserOperation.RemoveCommunity ||
671       op == UserOperation.FollowCommunity
672     ) {
673       let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
674       this.state.postRes.match({
675         some: res => {
676           res.community_view = data.community_view;
677           res.post_view.community = data.community_view.community;
678           this.setState(this.state);
679         },
680         none: void 0,
681       });
682     } else if (op == UserOperation.BanFromCommunity) {
683       let data = wsJsonToRes<BanFromCommunityResponse>(
684         msg,
685         BanFromCommunityResponse
686       );
687       this.state.postRes.match({
688         some: res => {
689           res.comments
690             .filter(c => c.creator.id == data.person_view.person.id)
691             .forEach(c => (c.creator_banned_from_community = data.banned));
692           if (res.post_view.creator.id == data.person_view.person.id) {
693             res.post_view.creator_banned_from_community = data.banned;
694           }
695           this.setState(this.state);
696         },
697         none: void 0,
698       });
699     } else if (op == UserOperation.AddModToCommunity) {
700       let data = wsJsonToRes<AddModToCommunityResponse>(
701         msg,
702         AddModToCommunityResponse
703       );
704       this.state.postRes.match({
705         some: res => {
706           res.moderators = data.moderators;
707           this.setState(this.state);
708         },
709         none: void 0,
710       });
711     } else if (op == UserOperation.BanPerson) {
712       let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
713       this.state.postRes.match({
714         some: res => {
715           res.comments
716             .filter(c => c.creator.id == data.person_view.person.id)
717             .forEach(c => (c.creator.banned = data.banned));
718           if (res.post_view.creator.id == data.person_view.person.id) {
719             res.post_view.creator.banned = data.banned;
720           }
721           this.setState(this.state);
722         },
723         none: void 0,
724       });
725     } else if (op == UserOperation.AddAdmin) {
726       let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
727       this.state.siteRes.admins = data.admins;
728       this.setState(this.state);
729     } else if (op == UserOperation.Search) {
730       let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
731       let xPosts = data.posts.filter(
732         p => p.post.id != Number(this.props.match.params.id)
733       );
734       this.state.crossPosts = xPosts.length > 0 ? Some(xPosts) : None;
735       this.setState(this.state);
736     } else if (op == UserOperation.LeaveAdmin) {
737       let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
738       this.state.siteRes = data;
739       this.setState(this.state);
740     } else if (op == UserOperation.TransferCommunity) {
741       let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
742       this.state.postRes.match({
743         some: res => {
744           res.community_view = data.community_view;
745           res.post_view.community = data.community_view.community;
746           res.moderators = data.moderators;
747           this.setState(this.state);
748         },
749         none: void 0,
750       });
751     } else if (op == UserOperation.BlockPerson) {
752       let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
753       updatePersonBlock(data);
754     } else if (op == UserOperation.CreatePostReport) {
755       let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
756       if (data) {
757         toast(i18n.t("report_created"));
758       }
759     } else if (op == UserOperation.CreateCommentReport) {
760       let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
761       if (data) {
762         toast(i18n.t("report_created"));
763       }
764     } else if (
765       op == UserOperation.PurgePerson ||
766       op == UserOperation.PurgePost ||
767       op == UserOperation.PurgeComment ||
768       op == UserOperation.PurgeCommunity
769     ) {
770       let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
771       if (data.success) {
772         toast(i18n.t("purge_success"));
773         this.context.router.history.push(`/`);
774       }
775     }
776   }
777 }