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