]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/community.tsx
Upgrade inferno v8.0.0 try2 (#790)
[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         />
395       )
396     ) : this.state.commentsLoading ? (
397       <h5>
398         <Spinner large />
399       </h5>
400     ) : (
401       <CommentNodes
402         nodes={commentsToFlatNodes(this.state.comments)}
403         viewType={CommentViewType.Flat}
404         noIndent
405         showContext
406         enableDownvotes={enableDownvotes(this.state.siteRes)}
407         moderators={this.state.communityRes.map(r => r.moderators)}
408         admins={Some(this.state.siteRes.admins)}
409         maxCommentsShown={None}
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       this.state.posts.unshift(data.post_view);
596       if (
597         UserService.Instance.myUserInfo
598           .map(m => m.local_user_view.local_user.show_new_post_notifs)
599           .unwrapOr(false)
600       ) {
601         notifyPost(data.post_view, this.context.router);
602       }
603       this.setState(this.state);
604     } else if (op == UserOperation.CreatePostLike) {
605       let data = wsJsonToRes<PostResponse>(msg, PostResponse);
606       createPostLikeFindRes(data.post_view, this.state.posts);
607       this.setState(this.state);
608     } else if (op == UserOperation.AddModToCommunity) {
609       let data = wsJsonToRes<AddModToCommunityResponse>(
610         msg,
611         AddModToCommunityResponse
612       );
613       this.state.communityRes.match({
614         some: res => (res.moderators = data.moderators),
615         none: void 0,
616       });
617       this.setState(this.state);
618     } else if (op == UserOperation.BanFromCommunity) {
619       let data = wsJsonToRes<BanFromCommunityResponse>(
620         msg,
621         BanFromCommunityResponse
622       );
623
624       // TODO this might be incorrect
625       this.state.posts
626         .filter(p => p.creator.id == data.person_view.person.id)
627         .forEach(p => (p.creator_banned_from_community = data.banned));
628
629       this.setState(this.state);
630     } else if (op == UserOperation.GetComments) {
631       let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
632       this.setState({ comments: data.comments, commentsLoading: false });
633     } else if (
634       op == UserOperation.EditComment ||
635       op == UserOperation.DeleteComment ||
636       op == UserOperation.RemoveComment
637     ) {
638       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
639       editCommentRes(data.comment_view, this.state.comments);
640       this.setState(this.state);
641     } else if (op == UserOperation.CreateComment) {
642       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
643
644       // Necessary since it might be a user reply
645       if (data.form_id) {
646         this.state.comments.unshift(data.comment_view);
647         this.setState(this.state);
648       }
649     } else if (op == UserOperation.SaveComment) {
650       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
651       saveCommentRes(data.comment_view, this.state.comments);
652       this.setState(this.state);
653     } else if (op == UserOperation.CreateCommentLike) {
654       let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
655       createCommentLikeRes(data.comment_view, this.state.comments);
656       this.setState(this.state);
657     } else if (op == UserOperation.BlockPerson) {
658       let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
659       updatePersonBlock(data);
660     } else if (op == UserOperation.CreatePostReport) {
661       let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
662       if (data) {
663         toast(i18n.t("report_created"));
664       }
665     } else if (op == UserOperation.CreateCommentReport) {
666       let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
667       if (data) {
668         toast(i18n.t("report_created"));
669       }
670     } else if (op == UserOperation.PurgeCommunity) {
671       let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
672       if (data.success) {
673         toast(i18n.t("purge_success"));
674         this.context.router.history.push(`/`);
675       }
676     } else if (op == UserOperation.BlockCommunity) {
677       let data = wsJsonToRes<BlockCommunityResponse>(
678         msg,
679         BlockCommunityResponse
680       );
681       this.state.communityRes.match({
682         some: res => (res.community_view.blocked = data.blocked),
683         none: void 0,
684       });
685       updateCommunityBlock(data);
686       this.setState(this.state);
687     }
688   }
689 }