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