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