]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/community.tsx
Adding new site setup fields. (#840)
[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         `${res.community_view.community.title} - ${this.state.siteRes.site_view.site.name}`,
276       none: "",
277     });
278   }
279
280   render() {
281     return (
282       <div className="container-lg">
283         {this.state.communityLoading ? (
284           <h5>
285             <Spinner large />
286           </h5>
287         ) : (
288           this.state.communityRes.match({
289             some: res => (
290               <>
291                 <HtmlTags
292                   title={this.documentTitle}
293                   path={this.context.router.route.match.url}
294                   description={res.community_view.community.description}
295                   image={res.community_view.community.icon}
296                 />
297
298                 <div className="row">
299                   <div className="col-12 col-md-8">
300                     {this.communityInfo()}
301                     <div className="d-block d-md-none">
302                       <button
303                         className="btn btn-secondary d-inline-block mb-2 mr-3"
304                         onClick={linkEvent(this, this.handleShowSidebarMobile)}
305                       >
306                         {i18n.t("sidebar")}{" "}
307                         <Icon
308                           icon={
309                             this.state.showSidebarMobile
310                               ? `minus-square`
311                               : `plus-square`
312                           }
313                           classes="icon-inline"
314                         />
315                       </button>
316                       {this.state.showSidebarMobile && (
317                         <>
318                           <Sidebar
319                             community_view={res.community_view}
320                             moderators={res.moderators}
321                             admins={this.state.siteRes.admins}
322                             online={res.online}
323                             enableNsfw={enableNsfw(this.state.siteRes)}
324                           />
325                           {!res.community_view.community.local &&
326                             res.site.match({
327                               some: site => (
328                                 <SiteSidebar
329                                   site={site}
330                                   showLocal={showLocal(this.isoData)}
331                                   admins={None}
332                                   counts={None}
333                                   online={None}
334                                 />
335                               ),
336                               none: <></>,
337                             })}
338                         </>
339                       )}
340                     </div>
341                     {this.selects()}
342                     {this.listings()}
343                     <Paginator
344                       page={this.state.page}
345                       onChange={this.handlePageChange}
346                     />
347                   </div>
348                   <div className="d-none d-md-block col-md-4">
349                     <Sidebar
350                       community_view={res.community_view}
351                       moderators={res.moderators}
352                       admins={this.state.siteRes.admins}
353                       online={res.online}
354                       enableNsfw={enableNsfw(this.state.siteRes)}
355                     />
356                     {!res.community_view.community.local &&
357                       res.site.match({
358                         some: site => (
359                           <SiteSidebar
360                             site={site}
361                             showLocal={showLocal(this.isoData)}
362                             admins={None}
363                             counts={None}
364                             online={None}
365                           />
366                         ),
367                         none: <></>,
368                       })}
369                   </div>
370                 </div>
371               </>
372             ),
373             none: <></>,
374           })
375         )}
376       </div>
377     );
378   }
379
380   listings() {
381     return this.state.dataType == DataType.Post ? (
382       this.state.postsLoading ? (
383         <h5>
384           <Spinner large />
385         </h5>
386       ) : (
387         <PostListings
388           posts={this.state.posts}
389           removeDuplicates
390           enableDownvotes={enableDownvotes(this.state.siteRes)}
391           enableNsfw={enableNsfw(this.state.siteRes)}
392           allLanguages={this.state.siteRes.all_languages}
393         />
394       )
395     ) : this.state.commentsLoading ? (
396       <h5>
397         <Spinner large />
398       </h5>
399     ) : (
400       <CommentNodes
401         nodes={commentsToFlatNodes(this.state.comments)}
402         viewType={CommentViewType.Flat}
403         noIndent
404         showContext
405         enableDownvotes={enableDownvotes(this.state.siteRes)}
406         moderators={this.state.communityRes.map(r => r.moderators)}
407         admins={Some(this.state.siteRes.admins)}
408         maxCommentsShown={None}
409         allLanguages={this.state.siteRes.all_languages}
410       />
411     );
412   }
413
414   communityInfo() {
415     return this.state.communityRes
416       .map(r => r.community_view.community)
417       .match({
418         some: community => (
419           <div className="mb-2">
420             <BannerIconHeader banner={community.banner} icon={community.icon} />
421             <h5 className="mb-0 overflow-wrap-anywhere">{community.title}</h5>
422             <CommunityLink
423               community={community}
424               realLink
425               useApubName
426               muted
427               hideAvatar
428             />
429           </div>
430         ),
431         none: <></>,
432       });
433   }
434
435   selects() {
436     let communityRss = this.state.communityRes.map(r =>
437       communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
438     );
439     return (
440       <div className="mb-3">
441         <span className="mr-3">
442           <DataTypeSelect
443             type_={this.state.dataType}
444             onChange={this.handleDataTypeChange}
445           />
446         </span>
447         <span className="mr-2">
448           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
449         </span>
450         {communityRss.match({
451           some: rss => (
452             <>
453               <a href={rss} title="RSS" rel={relTags}>
454                 <Icon icon="rss" classes="text-muted small" />
455               </a>
456               <link rel="alternate" type="application/atom+xml" href={rss} />
457             </>
458           ),
459           none: <></>,
460         })}
461       </div>
462     );
463   }
464
465   handlePageChange(page: number) {
466     this.updateUrl({ page });
467     window.scrollTo(0, 0);
468   }
469
470   handleSortChange(val: SortType) {
471     this.updateUrl({ sort: val, page: 1 });
472     window.scrollTo(0, 0);
473   }
474
475   handleDataTypeChange(val: DataType) {
476     this.updateUrl({ dataType: DataType[val], page: 1 });
477     window.scrollTo(0, 0);
478   }
479
480   handleShowSidebarMobile(i: Community) {
481     i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
482   }
483
484   updateUrl(paramUpdates: UrlParams) {
485     const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
486     const sortStr = paramUpdates.sort || this.state.sort;
487     const page = paramUpdates.page || this.state.page;
488
489     let typeView = `/c/${this.state.communityName}`;
490
491     this.props.history.push(
492       `${typeView}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
493     );
494   }
495
496   fetchData() {
497     if (this.state.dataType == DataType.Post) {
498       let form = new GetPosts({
499         page: Some(this.state.page),
500         limit: Some(fetchLimit),
501         sort: Some(this.state.sort),
502         type_: Some(ListingType.All),
503         community_name: Some(this.state.communityName),
504         community_id: None,
505         saved_only: Some(false),
506         auth: auth(false).ok(),
507       });
508       WebSocketService.Instance.send(wsClient.getPosts(form));
509     } else {
510       let form = new GetComments({
511         page: Some(this.state.page),
512         limit: Some(fetchLimit),
513         max_depth: None,
514         sort: Some(postToCommentSortType(this.state.sort)),
515         type_: Some(ListingType.All),
516         community_name: Some(this.state.communityName),
517         community_id: None,
518         saved_only: Some(false),
519         post_id: None,
520         parent_id: None,
521         auth: auth(false).ok(),
522       });
523       WebSocketService.Instance.send(wsClient.getComments(form));
524     }
525   }
526
527   parseMessage(msg: any) {
528     let op = wsUserOp(msg);
529     console.log(msg);
530     if (msg.error) {
531       toast(i18n.t(msg.error), "danger");
532       this.context.router.history.push("/");
533       return;
534     } else if (msg.reconnect) {
535       this.state.communityRes.match({
536         some: res => {
537           WebSocketService.Instance.send(
538             wsClient.communityJoin({
539               community_id: res.community_view.community.id,
540             })
541           );
542         },
543         none: void 0,
544       });
545       this.fetchData();
546     } else if (op == UserOperation.GetCommunity) {
547       let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
548       this.setState({ communityRes: Some(data), communityLoading: false });
549       // TODO why is there no auth in this form?
550       WebSocketService.Instance.send(
551         wsClient.communityJoin({
552           community_id: data.community_view.community.id,
553         })
554       );
555     } else if (
556       op == UserOperation.EditCommunity ||
557       op == UserOperation.DeleteCommunity ||
558       op == UserOperation.RemoveCommunity
559     ) {
560       let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
561       this.state.communityRes.match({
562         some: res => (res.community_view = data.community_view),
563         none: void 0,
564       });
565       this.setState(this.state);
566     } else if (op == UserOperation.FollowCommunity) {
567       let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
568       this.state.communityRes.match({
569         some: res => {
570           res.community_view.subscribed = data.community_view.subscribed;
571           res.community_view.counts.subscribers =
572             data.community_view.counts.subscribers;
573         },
574         none: void 0,
575       });
576       this.setState(this.state);
577     } else if (op == UserOperation.GetPosts) {
578       let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse);
579       this.setState({ posts: data.posts, postsLoading: false });
580       restoreScrollPosition(this.context);
581       setupTippy();
582     } else if (
583       op == UserOperation.EditPost ||
584       op == UserOperation.DeletePost ||
585       op == UserOperation.RemovePost ||
586       op == UserOperation.LockPost ||
587       op == UserOperation.StickyPost ||
588       op == UserOperation.SavePost
589     ) {
590       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
591       editPostFindRes(data.post_view, this.state.posts);
592       this.setState(this.state);
593     } else if (op == UserOperation.CreatePost) {
594       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
595
596       let showPostNotifs = UserService.Instance.myUserInfo
597         .map(m => m.local_user_view.local_user.show_new_post_notifs)
598         .unwrapOr(false);
599
600       // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
601       //
602       if (
603         this.state.page == 1 &&
604         nsfwCheck(data.post_view) &&
605         !isPostBlocked(data.post_view)
606       ) {
607         this.state.posts.unshift(data.post_view);
608         if (showPostNotifs) {
609           notifyPost(data.post_view, this.context.router);
610         }
611         this.setState(this.state);
612       }
613     } else if (op == UserOperation.CreatePostLike) {
614       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
615       createPostLikeFindRes(data.post_view, this.state.posts);
616       this.setState(this.state);
617     } else if (op == UserOperation.AddModToCommunity) {
618       let data = wsJsonToRes<AddModToCommunityResponse>(
619         msg,
620         AddModToCommunityResponse
621       );
622       this.state.communityRes.match({
623         some: res => (res.moderators = data.moderators),
624         none: void 0,
625       });
626       this.setState(this.state);
627     } else if (op == UserOperation.BanFromCommunity) {
628       let data = wsJsonToRes<BanFromCommunityResponse>(
629         msg,
630         BanFromCommunityResponse
631       );
632
633       // TODO this might be incorrect
634       this.state.posts
635         .filter(p => p.creator.id == data.person_view.person.id)
636         .forEach(p => (p.creator_banned_from_community = data.banned));
637
638       this.setState(this.state);
639     } else if (op == UserOperation.GetComments) {
640       let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
641       this.setState({ comments: data.comments, commentsLoading: false });
642     } else if (
643       op == UserOperation.EditComment ||
644       op == UserOperation.DeleteComment ||
645       op == UserOperation.RemoveComment
646     ) {
647       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
648       editCommentRes(data.comment_view, this.state.comments);
649       this.setState(this.state);
650     } else if (op == UserOperation.CreateComment) {
651       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
652
653       // Necessary since it might be a user reply
654       if (data.form_id) {
655         this.state.comments.unshift(data.comment_view);
656         this.setState(this.state);
657       }
658     } else if (op == UserOperation.SaveComment) {
659       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
660       saveCommentRes(data.comment_view, this.state.comments);
661       this.setState(this.state);
662     } else if (op == UserOperation.CreateCommentLike) {
663       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
664       createCommentLikeRes(data.comment_view, this.state.comments);
665       this.setState(this.state);
666     } else if (op == UserOperation.BlockPerson) {
667       let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
668       updatePersonBlock(data);
669     } else if (op == UserOperation.CreatePostReport) {
670       let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
671       if (data) {
672         toast(i18n.t("report_created"));
673       }
674     } else if (op == UserOperation.CreateCommentReport) {
675       let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
676       if (data) {
677         toast(i18n.t("report_created"));
678       }
679     } else if (op == UserOperation.PurgeCommunity) {
680       let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
681       if (data.success) {
682         toast(i18n.t("purge_success"));
683         this.context.router.history.push(`/`);
684       }
685     } else if (op == UserOperation.BlockCommunity) {
686       let data = wsJsonToRes<BlockCommunityResponse>(
687         msg,
688         BlockCommunityResponse
689       );
690       this.state.communityRes.match({
691         some: res => (res.community_view.blocked = data.blocked),
692         none: void 0,
693       });
694       updateCommunityBlock(data);
695       this.setState(this.state);
696     }
697   }
698 }