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