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