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