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