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