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