]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community.tsx
Change from using Link to NavLink. resolve #269
[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.localUserView
181       ? Object.values(SortType)[
182           UserService.Instance.localUserView.local_user.default_sort_type
183         ]
184       : SortType.Active;
185
186     let page = pathSplit[8] ? Number(pathSplit[8]) : 1;
187
188     if (dataType == DataType.Post) {
189       let getPostsForm: GetPosts = {
190         page,
191         limit: fetchLimit,
192         sort,
193         type_: ListingType.Community,
194         saved_only: false,
195       };
196       setOptionalAuth(getPostsForm, req.auth);
197       this.setIdOrName(getPostsForm, id, name_);
198       promises.push(req.client.getPosts(getPostsForm));
199     } else {
200       let getCommentsForm: GetComments = {
201         page,
202         limit: fetchLimit,
203         sort,
204         type_: ListingType.Community,
205         saved_only: false,
206       };
207       setOptionalAuth(getCommentsForm, req.auth);
208       this.setIdOrName(getCommentsForm, id, name_);
209       promises.push(req.client.getComments(getCommentsForm));
210     }
211
212     return promises;
213   }
214
215   static setIdOrName(obj: any, id: number, name_: string) {
216     if (id) {
217       obj.community_id = id;
218     } else {
219       obj.community_name = name_;
220     }
221   }
222
223   componentDidUpdate(_: any, lastState: State) {
224     if (
225       lastState.dataType !== this.state.dataType ||
226       lastState.sort !== this.state.sort ||
227       lastState.page !== this.state.page
228     ) {
229       this.setState({ postsLoading: true, commentsLoading: true });
230       this.fetchData();
231     }
232   }
233
234   get documentTitle(): string {
235     return `${this.state.communityRes.community_view.community.title} - ${this.state.siteRes.site_view.site.name}`;
236   }
237
238   render() {
239     let cv = this.state.communityRes?.community_view;
240     return (
241       <div class="container">
242         {this.state.communityLoading ? (
243           <h5>
244             <Spinner />
245           </h5>
246         ) : (
247           <div class="row">
248             <div class="col-12 col-md-8">
249               <HtmlTags
250                 title={this.documentTitle}
251                 path={this.context.router.route.match.url}
252                 description={cv.community.description}
253                 image={cv.community.icon}
254               />
255               {this.communityInfo()}
256               {this.selects()}
257               {this.listings()}
258               {this.paginator()}
259             </div>
260             <div class="col-12 col-md-4">
261               <Sidebar
262                 community_view={cv}
263                 moderators={this.state.communityRes.moderators}
264                 admins={this.state.siteRes.admins}
265                 online={this.state.communityRes.online}
266                 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
267               />
268             </div>
269           </div>
270         )}
271       </div>
272     );
273   }
274
275   listings() {
276     let site = this.state.siteRes.site_view.site;
277     return this.state.dataType == DataType.Post ? (
278       this.state.postsLoading ? (
279         <h5>
280           <Spinner />
281         </h5>
282       ) : (
283         <PostListings
284           posts={this.state.posts}
285           removeDuplicates
286           enableDownvotes={site.enable_downvotes}
287           enableNsfw={site.enable_nsfw}
288         />
289       )
290     ) : this.state.commentsLoading ? (
291       <h5>
292         <Spinner />
293       </h5>
294     ) : (
295       <CommentNodes
296         nodes={commentsToFlatNodes(this.state.comments)}
297         noIndent
298         showContext
299         enableDownvotes={site.enable_downvotes}
300       />
301     );
302   }
303
304   communityInfo() {
305     let community = this.state.communityRes.community_view.community;
306     return (
307       <div>
308         <BannerIconHeader banner={community.banner} icon={community.icon} />
309         <h5 class="mb-0">{community.title}</h5>
310         <CommunityLink
311           community={community}
312           realLink
313           useApubName
314           muted
315           hideAvatar
316         />
317         <hr />
318       </div>
319     );
320   }
321
322   selects() {
323     return (
324       <div class="mb-3">
325         <span class="mr-3">
326           <DataTypeSelect
327             type_={this.state.dataType}
328             onChange={this.handleDataTypeChange}
329           />
330         </span>
331         <span class="mr-2">
332           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
333         </span>
334         <a
335           href={communityRSSUrl(
336             this.state.communityRes.community_view.community.actor_id,
337             this.state.sort
338           )}
339           title="RSS"
340           rel="noopener"
341         >
342           <Icon icon="rss" classes="text-muted small" />
343         </a>
344       </div>
345     );
346   }
347
348   paginator() {
349     return (
350       <div class="my-2">
351         {this.state.page > 1 && (
352           <button
353             class="btn btn-secondary mr-1"
354             onClick={linkEvent(this, this.prevPage)}
355           >
356             {i18n.t("prev")}
357           </button>
358         )}
359         {this.state.posts.length > 0 && (
360           <button
361             class="btn btn-secondary"
362             onClick={linkEvent(this, this.nextPage)}
363           >
364             {i18n.t("next")}
365           </button>
366         )}
367       </div>
368     );
369   }
370
371   nextPage(i: Community) {
372     i.updateUrl({ page: i.state.page + 1 });
373     window.scrollTo(0, 0);
374   }
375
376   prevPage(i: Community) {
377     i.updateUrl({ page: i.state.page - 1 });
378     window.scrollTo(0, 0);
379   }
380
381   handleSortChange(val: SortType) {
382     this.updateUrl({ sort: val, page: 1 });
383     window.scrollTo(0, 0);
384   }
385
386   handleDataTypeChange(val: DataType) {
387     this.updateUrl({ dataType: DataType[val], page: 1 });
388     window.scrollTo(0, 0);
389   }
390
391   updateUrl(paramUpdates: UrlParams) {
392     const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
393     const sortStr = paramUpdates.sort || this.state.sort;
394     const page = paramUpdates.page || this.state.page;
395
396     let typeView = this.state.communityName
397       ? `/c/${this.state.communityName}`
398       : `/community/${this.state.communityId}`;
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_id: this.state.communityId,
413         community_name: this.state.communityName,
414         saved_only: false,
415         auth: authField(false),
416       };
417       WebSocketService.Instance.send(wsClient.getPosts(form));
418     } else {
419       let form: GetComments = {
420         page: this.state.page,
421         limit: fetchLimit,
422         sort: this.state.sort,
423         type_: ListingType.Community,
424         community_id: this.state.communityId,
425         community_name: this.state.communityName,
426         saved_only: false,
427         auth: authField(false),
428       };
429       WebSocketService.Instance.send(wsClient.getComments(form));
430     }
431   }
432
433   parseMessage(msg: any) {
434     let op = wsUserOp(msg);
435     console.log(msg);
436     if (msg.error) {
437       toast(i18n.t(msg.error), "danger");
438       this.context.router.history.push("/");
439       return;
440     } else if (msg.reconnect) {
441       WebSocketService.Instance.send(
442         wsClient.communityJoin({
443           community_id: this.state.communityRes.community_view.community.id,
444         })
445       );
446       this.fetchData();
447     } else if (op == UserOperation.GetCommunity) {
448       let data = wsJsonToRes<GetCommunityResponse>(msg).data;
449       this.state.communityRes = data;
450       this.state.communityLoading = false;
451       this.setState(this.state);
452       // TODO why is there no auth in this form?
453       WebSocketService.Instance.send(
454         wsClient.communityJoin({
455           community_id: data.community_view.community.id,
456         })
457       );
458     } else if (
459       op == UserOperation.EditCommunity ||
460       op == UserOperation.DeleteCommunity ||
461       op == UserOperation.RemoveCommunity
462     ) {
463       let data = wsJsonToRes<CommunityResponse>(msg).data;
464       this.state.communityRes.community_view = data.community_view;
465       this.setState(this.state);
466     } else if (op == UserOperation.FollowCommunity) {
467       let data = wsJsonToRes<CommunityResponse>(msg).data;
468       this.state.communityRes.community_view.subscribed =
469         data.community_view.subscribed;
470       this.state.communityRes.community_view.counts.subscribers =
471         data.community_view.counts.subscribers;
472       this.setState(this.state);
473     } else if (op == UserOperation.GetPosts) {
474       let data = wsJsonToRes<GetPostsResponse>(msg).data;
475       this.state.posts = data.posts;
476       this.state.postsLoading = false;
477       this.setState(this.state);
478       restoreScrollPosition(this.context);
479       setupTippy();
480     } else if (
481       op == UserOperation.EditPost ||
482       op == UserOperation.DeletePost ||
483       op == UserOperation.RemovePost ||
484       op == UserOperation.LockPost ||
485       op == UserOperation.StickyPost ||
486       op == UserOperation.SavePost
487     ) {
488       let data = wsJsonToRes<PostResponse>(msg).data;
489       editPostFindRes(data.post_view, this.state.posts);
490       this.setState(this.state);
491     } else if (op == UserOperation.CreatePost) {
492       let data = wsJsonToRes<PostResponse>(msg).data;
493       this.state.posts.unshift(data.post_view);
494       notifyPost(data.post_view, this.context.router);
495       this.setState(this.state);
496     } else if (op == UserOperation.CreatePostLike) {
497       let data = wsJsonToRes<PostResponse>(msg).data;
498       createPostLikeFindRes(data.post_view, this.state.posts);
499       this.setState(this.state);
500     } else if (op == UserOperation.AddModToCommunity) {
501       let data = wsJsonToRes<AddModToCommunityResponse>(msg).data;
502       this.state.communityRes.moderators = data.moderators;
503       this.setState(this.state);
504     } else if (op == UserOperation.BanFromCommunity) {
505       let data = wsJsonToRes<BanFromCommunityResponse>(msg).data;
506
507       // TODO this might be incorrect
508       this.state.posts
509         .filter(p => p.creator.id == data.person_view.person.id)
510         .forEach(p => (p.creator_banned_from_community = data.banned));
511
512       this.setState(this.state);
513     } else if (op == UserOperation.GetComments) {
514       let data = wsJsonToRes<GetCommentsResponse>(msg).data;
515       this.state.comments = data.comments;
516       this.state.commentsLoading = false;
517       this.setState(this.state);
518     } else if (
519       op == UserOperation.EditComment ||
520       op == UserOperation.DeleteComment ||
521       op == UserOperation.RemoveComment
522     ) {
523       let data = wsJsonToRes<CommentResponse>(msg).data;
524       editCommentRes(data.comment_view, this.state.comments);
525       this.setState(this.state);
526     } else if (op == UserOperation.CreateComment) {
527       let data = wsJsonToRes<CommentResponse>(msg).data;
528
529       // Necessary since it might be a user reply
530       if (data.form_id) {
531         this.state.comments.unshift(data.comment_view);
532         this.setState(this.state);
533       }
534     } else if (op == UserOperation.SaveComment) {
535       let data = wsJsonToRes<CommentResponse>(msg).data;
536       saveCommentRes(data.comment_view, this.state.comments);
537       this.setState(this.state);
538     } else if (op == UserOperation.CreateCommentLike) {
539       let data = wsJsonToRes<CommentResponse>(msg).data;
540       createCommentLikeRes(data.comment_view, this.state.comments);
541       this.setState(this.state);
542     }
543   }
544 }