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