]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/community.tsx
Fix posts pushed from blocked users/comms. Fixes #697 (#792)
[lemmy-ui.git] / src / shared / components / community / community.tsx
1 import { None, Option, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
3 import {
4   AddModToCommunityResponse,
5   BanFromCommunityResponse,
6   BlockCommunityResponse,
7   BlockPersonResponse,
8   CommentReportResponse,
9   CommentResponse,
10   CommentView,
11   CommunityResponse,
12   GetComments,
13   GetCommentsResponse,
14   GetCommunity,
15   GetCommunityResponse,
16   GetPosts,
17   GetPostsResponse,
18   GetSiteResponse,
19   ListingType,
20   PostReportResponse,
21   PostResponse,
22   PostView,
23   PurgeItemResponse,
24   SortType,
25   toOption,
26   UserOperation,
27   wsJsonToRes,
28   wsUserOp,
29 } from "lemmy-js-client";
30 import { Subscription } from "rxjs";
31 import { i18n } from "../../i18next";
32 import {
33   CommentViewType,
34   DataType,
35   InitialFetchRequest,
36 } from "../../interfaces";
37 import { UserService, WebSocketService } from "../../services";
38 import {
39   auth,
40   commentsToFlatNodes,
41   communityRSSUrl,
42   createCommentLikeRes,
43   createPostLikeFindRes,
44   editCommentRes,
45   editPostFindRes,
46   enableDownvotes,
47   enableNsfw,
48   fetchLimit,
49   getDataTypeFromProps,
50   getPageFromProps,
51   getSortTypeFromProps,
52   isPostBlocked,
53   notifyPost,
54   nsfwCheck,
55   postToCommentSortType,
56   relTags,
57   restoreScrollPosition,
58   saveCommentRes,
59   saveScrollPosition,
60   setIsoData,
61   setupTippy,
62   showLocal,
63   toast,
64   updateCommunityBlock,
65   updatePersonBlock,
66   wsClient,
67   wsSubscribe,
68 } from "../../utils";
69 import { CommentNodes } from "../comment/comment-nodes";
70 import { BannerIconHeader } from "../common/banner-icon-header";
71 import { DataTypeSelect } from "../common/data-type-select";
72 import { HtmlTags } from "../common/html-tags";
73 import { Icon, Spinner } from "../common/icon";
74 import { Paginator } from "../common/paginator";
75 import { SortSelect } from "../common/sort-select";
76 import { Sidebar } from "../community/sidebar";
77 import { SiteSidebar } from "../home/site-sidebar";
78 import { PostListings } from "../post/post-listings";
79 import { CommunityLink } from "./community-link";
80
81 interface State {
82   communityRes: Option<GetCommunityResponse>;
83   siteRes: GetSiteResponse;
84   communityName: string;
85   communityLoading: boolean;
86   postsLoading: boolean;
87   commentsLoading: boolean;
88   posts: PostView[];
89   comments: CommentView[];
90   dataType: DataType;
91   sort: SortType;
92   page: number;
93   showSidebarMobile: boolean;
94 }
95
96 interface CommunityProps {
97   dataType: DataType;
98   sort: SortType;
99   page: number;
100 }
101
102 interface UrlParams {
103   dataType?: string;
104   sort?: SortType;
105   page?: number;
106 }
107
108 export class Community extends Component<any, State> {
109   private isoData = setIsoData(
110     this.context,
111     GetCommunityResponse,
112     GetPostsResponse,
113     GetCommentsResponse
114   );
115   private subscription: Subscription;
116   private emptyState: State = {
117     communityRes: None,
118     communityName: this.props.match.params.name,
119     communityLoading: true,
120     postsLoading: true,
121     commentsLoading: true,
122     posts: [],
123     comments: [],
124     dataType: getDataTypeFromProps(this.props),
125     sort: getSortTypeFromProps(this.props),
126     page: getPageFromProps(this.props),
127     siteRes: this.isoData.site_res,
128     showSidebarMobile: false,
129   };
130
131   constructor(props: any, context: any) {
132     super(props, context);
133
134     this.state = this.emptyState;
135     this.handleSortChange = this.handleSortChange.bind(this);
136     this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
137     this.handlePageChange = this.handlePageChange.bind(this);
138
139     this.parseMessage = this.parseMessage.bind(this);
140     this.subscription = wsSubscribe(this.parseMessage);
141
142     // Only fetch the data if coming from another route
143     if (this.isoData.path == this.context.router.route.match.url) {
144       this.state = {
145         ...this.state,
146         communityRes: Some(this.isoData.routeData[0] as GetCommunityResponse),
147       };
148       let postsRes = Some(this.isoData.routeData[1] as GetPostsResponse);
149       let commentsRes = Some(this.isoData.routeData[2] as GetCommentsResponse);
150
151       if (postsRes.isSome()) {
152         this.state = { ...this.state, posts: postsRes.unwrap().posts };
153       }
154
155       if (commentsRes.isSome()) {
156         this.state = { ...this.state, comments: commentsRes.unwrap().comments };
157       }
158
159       this.state = {
160         ...this.state,
161         communityLoading: false,
162         postsLoading: false,
163         commentsLoading: false,
164       };
165     } else {
166       this.fetchCommunity();
167       this.fetchData();
168     }
169   }
170
171   fetchCommunity() {
172     let form = new GetCommunity({
173       name: Some(this.state.communityName),
174       id: None,
175       auth: auth(false).ok(),
176     });
177     WebSocketService.Instance.send(wsClient.getCommunity(form));
178   }
179
180   componentDidMount() {
181     setupTippy();
182   }
183
184   componentWillUnmount() {
185     saveScrollPosition(this.context);
186     this.subscription.unsubscribe();
187   }
188
189   static getDerivedStateFromProps(props: any): CommunityProps {
190     return {
191       dataType: getDataTypeFromProps(props),
192       sort: getSortTypeFromProps(props),
193       page: getPageFromProps(props),
194     };
195   }
196
197   static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
198     let pathSplit = req.path.split("/");
199     let promises: Promise<any>[] = [];
200
201     let communityName = pathSplit[2];
202     let communityForm = new GetCommunity({
203       name: Some(communityName),
204       id: None,
205       auth: req.auth,
206     });
207     promises.push(req.client.getCommunity(communityForm));
208
209     let dataType: DataType = pathSplit[4]
210       ? DataType[pathSplit[4]]
211       : DataType.Post;
212
213     let sort: Option<SortType> = toOption(
214       pathSplit[6]
215         ? SortType[pathSplit[6]]
216         : UserService.Instance.myUserInfo.match({
217             some: mui =>
218               Object.values(SortType)[
219                 mui.local_user_view.local_user.default_sort_type
220               ],
221             none: SortType.Active,
222           })
223     );
224
225     let page = toOption(pathSplit[8] ? Number(pathSplit[8]) : 1);
226
227     if (dataType == DataType.Post) {
228       let getPostsForm = new GetPosts({
229         community_name: Some(communityName),
230         community_id: None,
231         page,
232         limit: Some(fetchLimit),
233         sort,
234         type_: Some(ListingType.All),
235         saved_only: Some(false),
236         auth: req.auth,
237       });
238       promises.push(req.client.getPosts(getPostsForm));
239       promises.push(Promise.resolve());
240     } else {
241       let getCommentsForm = new GetComments({
242         community_name: Some(communityName),
243         community_id: None,
244         page,
245         limit: Some(fetchLimit),
246         max_depth: None,
247         sort: sort.map(postToCommentSortType),
248         type_: Some(ListingType.All),
249         saved_only: Some(false),
250         post_id: None,
251         parent_id: None,
252         auth: req.auth,
253       });
254       promises.push(Promise.resolve());
255       promises.push(req.client.getComments(getCommentsForm));
256     }
257
258     return promises;
259   }
260
261   componentDidUpdate(_: any, lastState: State) {
262     if (
263       lastState.dataType !== this.state.dataType ||
264       lastState.sort !== this.state.sort ||
265       lastState.page !== this.state.page
266     ) {
267       this.setState({ postsLoading: true, commentsLoading: true });
268       this.fetchData();
269     }
270   }
271
272   get documentTitle(): string {
273     return this.state.communityRes.match({
274       some: res =>
275         this.state.siteRes.site_view.match({
276           some: siteView =>
277             `${res.community_view.community.title} - ${siteView.site.name}`,
278           none: "",
279         }),
280       none: "",
281     });
282   }
283
284   render() {
285     return (
286       <div className="container">
287         {this.state.communityLoading ? (
288           <h5>
289             <Spinner large />
290           </h5>
291         ) : (
292           this.state.communityRes.match({
293             some: res => (
294               <>
295                 <HtmlTags
296                   title={this.documentTitle}
297                   path={this.context.router.route.match.url}
298                   description={res.community_view.community.description}
299                   image={res.community_view.community.icon}
300                 />
301
302                 <div className="row">
303                   <div className="col-12 col-md-8">
304                     {this.communityInfo()}
305                     <div className="d-block d-md-none">
306                       <button
307                         className="btn btn-secondary d-inline-block mb-2 mr-3"
308                         onClick={linkEvent(this, this.handleShowSidebarMobile)}
309                       >
310                         {i18n.t("sidebar")}{" "}
311                         <Icon
312                           icon={
313                             this.state.showSidebarMobile
314                               ? `minus-square`
315                               : `plus-square`
316                           }
317                           classes="icon-inline"
318                         />
319                       </button>
320                       {this.state.showSidebarMobile && (
321                         <>
322                           <Sidebar
323                             community_view={res.community_view}
324                             moderators={res.moderators}
325                             admins={this.state.siteRes.admins}
326                             online={res.online}
327                             enableNsfw={enableNsfw(this.state.siteRes)}
328                           />
329                           {!res.community_view.community.local &&
330                             res.site.match({
331                               some: site => (
332                                 <SiteSidebar
333                                   site={site}
334                                   showLocal={showLocal(this.isoData)}
335                                   admins={None}
336                                   counts={None}
337                                   online={None}
338                                 />
339                               ),
340                               none: <></>,
341                             })}
342                         </>
343                       )}
344                     </div>
345                     {this.selects()}
346                     {this.listings()}
347                     <Paginator
348                       page={this.state.page}
349                       onChange={this.handlePageChange}
350                     />
351                   </div>
352                   <div className="d-none d-md-block col-md-4">
353                     <Sidebar
354                       community_view={res.community_view}
355                       moderators={res.moderators}
356                       admins={this.state.siteRes.admins}
357                       online={res.online}
358                       enableNsfw={enableNsfw(this.state.siteRes)}
359                     />
360                     {!res.community_view.community.local &&
361                       res.site.match({
362                         some: site => (
363                           <SiteSidebar
364                             site={site}
365                             showLocal={showLocal(this.isoData)}
366                             admins={None}
367                             counts={None}
368                             online={None}
369                           />
370                         ),
371                         none: <></>,
372                       })}
373                   </div>
374                 </div>
375               </>
376             ),
377             none: <></>,
378           })
379         )}
380       </div>
381     );
382   }
383
384   listings() {
385     return this.state.dataType == DataType.Post ? (
386       this.state.postsLoading ? (
387         <h5>
388           <Spinner large />
389         </h5>
390       ) : (
391         <PostListings
392           posts={this.state.posts}
393           removeDuplicates
394           enableDownvotes={enableDownvotes(this.state.siteRes)}
395           enableNsfw={enableNsfw(this.state.siteRes)}
396           allLanguages={this.state.siteRes.all_languages}
397         />
398       )
399     ) : this.state.commentsLoading ? (
400       <h5>
401         <Spinner large />
402       </h5>
403     ) : (
404       <CommentNodes
405         nodes={commentsToFlatNodes(this.state.comments)}
406         viewType={CommentViewType.Flat}
407         noIndent
408         showContext
409         enableDownvotes={enableDownvotes(this.state.siteRes)}
410         moderators={this.state.communityRes.map(r => r.moderators)}
411         admins={Some(this.state.siteRes.admins)}
412         maxCommentsShown={None}
413         allLanguages={this.state.siteRes.all_languages}
414       />
415     );
416   }
417
418   communityInfo() {
419     return this.state.communityRes
420       .map(r => r.community_view.community)
421       .match({
422         some: community => (
423           <div className="mb-2">
424             <BannerIconHeader banner={community.banner} icon={community.icon} />
425             <h5 className="mb-0 overflow-wrap-anywhere">{community.title}</h5>
426             <CommunityLink
427               community={community}
428               realLink
429               useApubName
430               muted
431               hideAvatar
432             />
433           </div>
434         ),
435         none: <></>,
436       });
437   }
438
439   selects() {
440     let communityRss = this.state.communityRes.map(r =>
441       communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
442     );
443     return (
444       <div className="mb-3">
445         <span className="mr-3">
446           <DataTypeSelect
447             type_={this.state.dataType}
448             onChange={this.handleDataTypeChange}
449           />
450         </span>
451         <span className="mr-2">
452           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
453         </span>
454         {communityRss.match({
455           some: rss => (
456             <>
457               <a href={rss} title="RSS" rel={relTags}>
458                 <Icon icon="rss" classes="text-muted small" />
459               </a>
460               <link rel="alternate" type="application/atom+xml" href={rss} />
461             </>
462           ),
463           none: <></>,
464         })}
465       </div>
466     );
467   }
468
469   handlePageChange(page: number) {
470     this.updateUrl({ page });
471     window.scrollTo(0, 0);
472   }
473
474   handleSortChange(val: SortType) {
475     this.updateUrl({ sort: val, page: 1 });
476     window.scrollTo(0, 0);
477   }
478
479   handleDataTypeChange(val: DataType) {
480     this.updateUrl({ dataType: DataType[val], page: 1 });
481     window.scrollTo(0, 0);
482   }
483
484   handleShowSidebarMobile(i: Community) {
485     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
486   }
487
488   updateUrl(paramUpdates: UrlParams) {
489     const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
490     const sortStr = paramUpdates.sort || this.state.sort;
491     const page = paramUpdates.page || this.state.page;
492
493     let typeView = `/c/${this.state.communityName}`;
494
495     this.props.history.push(
496       `${typeView}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
497     );
498   }
499
500   fetchData() {
501     if (this.state.dataType == DataType.Post) {
502       let form = new GetPosts({
503         page: Some(this.state.page),
504         limit: Some(fetchLimit),
505         sort: Some(this.state.sort),
506         type_: Some(ListingType.All),
507         community_name: Some(this.state.communityName),
508         community_id: None,
509         saved_only: Some(false),
510         auth: auth(false).ok(),
511       });
512       WebSocketService.Instance.send(wsClient.getPosts(form));
513     } else {
514       let form = new GetComments({
515         page: Some(this.state.page),
516         limit: Some(fetchLimit),
517         max_depth: None,
518         sort: Some(postToCommentSortType(this.state.sort)),
519         type_: Some(ListingType.All),
520         community_name: Some(this.state.communityName),
521         community_id: None,
522         saved_only: Some(false),
523         post_id: None,
524         parent_id: None,
525         auth: auth(false).ok(),
526       });
527       WebSocketService.Instance.send(wsClient.getComments(form));
528     }
529   }
530
531   parseMessage(msg: any) {
532     let op = wsUserOp(msg);
533     console.log(msg);
534     if (msg.error) {
535       toast(i18n.t(msg.error), "danger");
536       this.context.router.history.push("/");
537       return;
538     } else if (msg.reconnect) {
539       this.state.communityRes.match({
540         some: res => {
541           WebSocketService.Instance.send(
542             wsClient.communityJoin({
543               community_id: res.community_view.community.id,
544             })
545           );
546         },
547         none: void 0,
548       });
549       this.fetchData();
550     } else if (op == UserOperation.GetCommunity) {
551       let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
552       this.setState({ communityRes: Some(data), communityLoading: false });
553       // TODO why is there no auth in this form?
554       WebSocketService.Instance.send(
555         wsClient.communityJoin({
556           community_id: data.community_view.community.id,
557         })
558       );
559     } else if (
560       op == UserOperation.EditCommunity ||
561       op == UserOperation.DeleteCommunity ||
562       op == UserOperation.RemoveCommunity
563     ) {
564       let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
565       this.state.communityRes.match({
566         some: res => (res.community_view = data.community_view),
567         none: void 0,
568       });
569       this.setState(this.state);
570     } else if (op == UserOperation.FollowCommunity) {
571       let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
572       this.state.communityRes.match({
573         some: res => {
574           res.community_view.subscribed = data.community_view.subscribed;
575           res.community_view.counts.subscribers =
576             data.community_view.counts.subscribers;
577         },
578         none: void 0,
579       });
580       this.setState(this.state);
581     } else if (op == UserOperation.GetPosts) {
582       let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse);
583       this.setState({ posts: data.posts, postsLoading: false });
584       restoreScrollPosition(this.context);
585       setupTippy();
586     } else if (
587       op == UserOperation.EditPost ||
588       op == UserOperation.DeletePost ||
589       op == UserOperation.RemovePost ||
590       op == UserOperation.LockPost ||
591       op == UserOperation.StickyPost ||
592       op == UserOperation.SavePost
593     ) {
594       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
595       editPostFindRes(data.post_view, this.state.posts);
596       this.setState(this.state);
597     } else if (op == UserOperation.CreatePost) {
598       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
599
600       let showPostNotifs = UserService.Instance.myUserInfo
601         .map(m => m.local_user_view.local_user.show_new_post_notifs)
602         .unwrapOr(false);
603
604       // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
605       //
606       if (
607         this.state.page == 1 &&
608         nsfwCheck(data.post_view) &&
609         !isPostBlocked(data.post_view)
610       ) {
611         this.state.posts.unshift(data.post_view);
612         if (showPostNotifs) {
613           notifyPost(data.post_view, this.context.router);
614         }
615         this.setState(this.state);
616       }
617     } else if (op == UserOperation.CreatePostLike) {
618       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
619       createPostLikeFindRes(data.post_view, this.state.posts);
620       this.setState(this.state);
621     } else if (op == UserOperation.AddModToCommunity) {
622       let data = wsJsonToRes<AddModToCommunityResponse>(
623         msg,
624         AddModToCommunityResponse
625       );
626       this.state.communityRes.match({
627         some: res => (res.moderators = data.moderators),
628         none: void 0,
629       });
630       this.setState(this.state);
631     } else if (op == UserOperation.BanFromCommunity) {
632       let data = wsJsonToRes<BanFromCommunityResponse>(
633         msg,
634         BanFromCommunityResponse
635       );
636
637       // TODO this might be incorrect
638       this.state.posts
639         .filter(p => p.creator.id == data.person_view.person.id)
640         .forEach(p => (p.creator_banned_from_community = data.banned));
641
642       this.setState(this.state);
643     } else if (op == UserOperation.GetComments) {
644       let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
645       this.setState({ comments: data.comments, commentsLoading: false });
646     } else if (
647       op == UserOperation.EditComment ||
648       op == UserOperation.DeleteComment ||
649       op == UserOperation.RemoveComment
650     ) {
651       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
652       editCommentRes(data.comment_view, this.state.comments);
653       this.setState(this.state);
654     } else if (op == UserOperation.CreateComment) {
655       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
656
657       // Necessary since it might be a user reply
658       if (data.form_id) {
659         this.state.comments.unshift(data.comment_view);
660         this.setState(this.state);
661       }
662     } else if (op == UserOperation.SaveComment) {
663       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
664       saveCommentRes(data.comment_view, this.state.comments);
665       this.setState(this.state);
666     } else if (op == UserOperation.CreateCommentLike) {
667       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
668       createCommentLikeRes(data.comment_view, this.state.comments);
669       this.setState(this.state);
670     } else if (op == UserOperation.BlockPerson) {
671       let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
672       updatePersonBlock(data);
673     } else if (op == UserOperation.CreatePostReport) {
674       let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
675       if (data) {
676         toast(i18n.t("report_created"));
677       }
678     } else if (op == UserOperation.CreateCommentReport) {
679       let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
680       if (data) {
681         toast(i18n.t("report_created"));
682       }
683     } else if (op == UserOperation.PurgeCommunity) {
684       let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
685       if (data.success) {
686         toast(i18n.t("purge_success"));
687         this.context.router.history.push(`/`);
688       }
689     } else if (op == UserOperation.BlockCommunity) {
690       let data = wsJsonToRes<BlockCommunityResponse>(
691         msg,
692         BlockCommunityResponse
693       );
694       this.state.communityRes.match({
695         some: res => (res.community_view.blocked = data.blocked),
696         none: void 0,
697       });
698       updateCommunityBlock(data);
699       this.setState(this.state);
700     }
701   }
702 }