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