]> Untitled Git - lemmy.git/blob - ui/src/components/community.tsx
Merge remote-tracking branch 'weblate/main' into main
[lemmy.git] / ui / src / components / community.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Helmet } from 'inferno-helmet';
3 import { Subscription } from 'rxjs';
4 import { retryWhen, delay, take } from 'rxjs/operators';
5 import {
6   UserOperation,
7   Community as CommunityI,
8   GetCommunityResponse,
9   CommunityResponse,
10   CommunityUser,
11   UserView,
12   SortType,
13   Post,
14   GetPostsForm,
15   GetCommunityForm,
16   ListingType,
17   DataType,
18   GetPostsResponse,
19   PostResponse,
20   AddModToCommunityResponse,
21   BanFromCommunityResponse,
22   Comment,
23   GetCommentsForm,
24   GetCommentsResponse,
25   CommentResponse,
26   WebSocketJsonResponse,
27   GetSiteResponse,
28   Site,
29 } from '../interfaces';
30 import { WebSocketService } from '../services';
31 import { PostListings } from './post-listings';
32 import { CommentNodes } from './comment-nodes';
33 import { SortSelect } from './sort-select';
34 import { DataTypeSelect } from './data-type-select';
35 import { Sidebar } from './sidebar';
36 import { CommunityLink } from './community-link';
37 import { BannerIconHeader } from './banner-icon-header';
38 import {
39   wsJsonToRes,
40   fetchLimit,
41   toast,
42   getPageFromProps,
43   getSortTypeFromProps,
44   getDataTypeFromProps,
45   editCommentRes,
46   saveCommentRes,
47   createCommentLikeRes,
48   createPostLikeFindRes,
49   editPostFindRes,
50   commentsToFlatNodes,
51   setupTippy,
52   favIconUrl,
53   notifyPost,
54 } from '../utils';
55 import { i18n } from '../i18next';
56
57 interface State {
58   community: CommunityI;
59   communityId: number;
60   communityName: string;
61   moderators: Array<CommunityUser>;
62   admins: Array<UserView>;
63   online: number;
64   loading: boolean;
65   posts: Array<Post>;
66   comments: Array<Comment>;
67   dataType: DataType;
68   sort: SortType;
69   page: number;
70   site: Site;
71 }
72
73 interface CommunityProps {
74   dataType: DataType;
75   sort: SortType;
76   page: number;
77 }
78
79 interface UrlParams {
80   dataType?: string;
81   sort?: string;
82   page?: number;
83 }
84
85 export class Community extends Component<any, State> {
86   private subscription: Subscription;
87   private emptyState: State = {
88     community: {
89       id: null,
90       name: null,
91       title: null,
92       category_id: null,
93       category_name: null,
94       creator_id: null,
95       creator_name: null,
96       number_of_subscribers: null,
97       number_of_posts: null,
98       number_of_comments: null,
99       published: null,
100       removed: null,
101       nsfw: false,
102       deleted: null,
103       local: null,
104       actor_id: null,
105       last_refreshed_at: null,
106       creator_actor_id: null,
107       creator_local: null,
108     },
109     moderators: [],
110     admins: [],
111     communityId: Number(this.props.match.params.id),
112     communityName: this.props.match.params.name,
113     online: null,
114     loading: true,
115     posts: [],
116     comments: [],
117     dataType: getDataTypeFromProps(this.props),
118     sort: getSortTypeFromProps(this.props),
119     page: getPageFromProps(this.props),
120     site: {
121       id: undefined,
122       name: undefined,
123       creator_id: undefined,
124       published: undefined,
125       creator_name: undefined,
126       number_of_users: undefined,
127       number_of_posts: undefined,
128       number_of_comments: undefined,
129       number_of_communities: undefined,
130       enable_downvotes: undefined,
131       open_registration: undefined,
132       enable_nsfw: undefined,
133       icon: undefined,
134       banner: undefined,
135       creator_preferred_username: undefined,
136     },
137   };
138
139   constructor(props: any, context: any) {
140     super(props, context);
141
142     this.state = this.emptyState;
143     this.handleSortChange = this.handleSortChange.bind(this);
144     this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
145
146     this.subscription = WebSocketService.Instance.subject
147       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
148       .subscribe(
149         msg => this.parseMessage(msg),
150         err => console.error(err),
151         () => console.log('complete')
152       );
153
154     let form: GetCommunityForm = {
155       id: this.state.communityId ? this.state.communityId : null,
156       name: this.state.communityName ? this.state.communityName : null,
157     };
158     WebSocketService.Instance.getCommunity(form);
159     WebSocketService.Instance.getSite();
160   }
161
162   componentWillUnmount() {
163     this.subscription.unsubscribe();
164   }
165
166   static getDerivedStateFromProps(props: any): CommunityProps {
167     return {
168       dataType: getDataTypeFromProps(props),
169       sort: getSortTypeFromProps(props),
170       page: getPageFromProps(props),
171     };
172   }
173
174   componentDidUpdate(_: any, lastState: State) {
175     if (
176       lastState.dataType !== this.state.dataType ||
177       lastState.sort !== this.state.sort ||
178       lastState.page !== this.state.page
179     ) {
180       this.setState({ loading: true });
181       this.fetchData();
182     }
183   }
184
185   get documentTitle(): string {
186     if (this.state.community.title) {
187       return `${this.state.community.title} - ${this.state.site.name}`;
188     } else {
189       return 'Lemmy';
190     }
191   }
192
193   get favIcon(): string {
194     return this.state.site.icon ? this.state.site.icon : favIconUrl;
195   }
196
197   render() {
198     return (
199       <div class="container">
200         <Helmet title={this.documentTitle}>
201           <link
202             id="favicon"
203             rel="icon"
204             type="image/x-icon"
205             href={this.favIcon}
206           />
207         </Helmet>
208         {this.state.loading ? (
209           <h5>
210             <svg class="icon icon-spinner spin">
211               <use xlinkHref="#icon-spinner"></use>
212             </svg>
213           </h5>
214         ) : (
215           <div class="row">
216             <div class="col-12 col-md-8">
217               {this.communityInfo()}
218               {this.selects()}
219               {this.listings()}
220               {this.paginator()}
221             </div>
222             <div class="col-12 col-md-4">
223               <Sidebar
224                 community={this.state.community}
225                 moderators={this.state.moderators}
226                 admins={this.state.admins}
227                 online={this.state.online}
228                 enableNsfw={this.state.site.enable_nsfw}
229               />
230             </div>
231           </div>
232         )}
233       </div>
234     );
235   }
236
237   listings() {
238     return this.state.dataType == DataType.Post ? (
239       <PostListings
240         posts={this.state.posts}
241         removeDuplicates
242         sort={this.state.sort}
243         enableDownvotes={this.state.site.enable_downvotes}
244         enableNsfw={this.state.site.enable_nsfw}
245       />
246     ) : (
247       <CommentNodes
248         nodes={commentsToFlatNodes(this.state.comments)}
249         noIndent
250         sortType={this.state.sort}
251         showContext
252         enableDownvotes={this.state.site.enable_downvotes}
253       />
254     );
255   }
256
257   communityInfo() {
258     return (
259       <div>
260         <BannerIconHeader
261           banner={this.state.community.banner}
262           icon={this.state.community.icon}
263         />
264         <h5 class="mb-0">{this.state.community.title}</h5>
265         <CommunityLink
266           community={this.state.community}
267           realLink
268           useApubName
269           muted
270           hideAvatar
271         />
272         <hr />
273       </div>
274     );
275   }
276
277   selects() {
278     return (
279       <div class="mb-3">
280         <span class="mr-3">
281           <DataTypeSelect
282             type_={this.state.dataType}
283             onChange={this.handleDataTypeChange}
284           />
285         </span>
286         <span class="mr-2">
287           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
288         </span>
289         <a
290           href={`/feeds/c/${this.state.communityName}.xml?sort=${
291             SortType[this.state.sort]
292           }`}
293           target="_blank"
294           title="RSS"
295           rel="noopener"
296         >
297           <svg class="icon text-muted small">
298             <use xlinkHref="#icon-rss">#</use>
299           </svg>
300         </a>
301       </div>
302     );
303   }
304
305   paginator() {
306     return (
307       <div class="my-2">
308         {this.state.page > 1 && (
309           <button
310             class="btn btn-secondary mr-1"
311             onClick={linkEvent(this, this.prevPage)}
312           >
313             {i18n.t('prev')}
314           </button>
315         )}
316         {this.state.posts.length > 0 && (
317           <button
318             class="btn btn-secondary"
319             onClick={linkEvent(this, this.nextPage)}
320           >
321             {i18n.t('next')}
322           </button>
323         )}
324       </div>
325     );
326   }
327
328   nextPage(i: Community) {
329     i.updateUrl({ page: i.state.page + 1 });
330     window.scrollTo(0, 0);
331   }
332
333   prevPage(i: Community) {
334     i.updateUrl({ page: i.state.page - 1 });
335     window.scrollTo(0, 0);
336   }
337
338   handleSortChange(val: SortType) {
339     this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
340     window.scrollTo(0, 0);
341   }
342
343   handleDataTypeChange(val: DataType) {
344     this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
345     window.scrollTo(0, 0);
346   }
347
348   updateUrl(paramUpdates: UrlParams) {
349     const dataTypeStr =
350       paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
351     const sortStr =
352       paramUpdates.sort || SortType[this.state.sort].toLowerCase();
353     const page = paramUpdates.page || this.state.page;
354     this.props.history.push(
355       `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
356     );
357   }
358
359   fetchData() {
360     if (this.state.dataType == DataType.Post) {
361       let getPostsForm: GetPostsForm = {
362         page: this.state.page,
363         limit: fetchLimit,
364         sort: SortType[this.state.sort],
365         type_: ListingType[ListingType.Community],
366         community_id: this.state.community.id,
367       };
368       WebSocketService.Instance.getPosts(getPostsForm);
369     } else {
370       let getCommentsForm: GetCommentsForm = {
371         page: this.state.page,
372         limit: fetchLimit,
373         sort: SortType[this.state.sort],
374         type_: ListingType[ListingType.Community],
375         community_id: this.state.community.id,
376       };
377       WebSocketService.Instance.getComments(getCommentsForm);
378     }
379   }
380
381   parseMessage(msg: WebSocketJsonResponse) {
382     console.log(msg);
383     let res = wsJsonToRes(msg);
384     if (msg.error) {
385       toast(i18n.t(msg.error), 'danger');
386       this.context.router.history.push('/');
387       return;
388     } else if (msg.reconnect) {
389       this.fetchData();
390     } else if (res.op == UserOperation.GetCommunity) {
391       let data = res.data as GetCommunityResponse;
392       this.state.community = data.community;
393       this.state.moderators = data.moderators;
394       this.state.online = data.online;
395       this.setState(this.state);
396       this.fetchData();
397     } else if (
398       res.op == UserOperation.EditCommunity ||
399       res.op == UserOperation.DeleteCommunity ||
400       res.op == UserOperation.RemoveCommunity
401     ) {
402       let data = res.data as CommunityResponse;
403       this.state.community = data.community;
404       this.setState(this.state);
405     } else if (res.op == UserOperation.FollowCommunity) {
406       let data = res.data as CommunityResponse;
407       this.state.community.subscribed = data.community.subscribed;
408       this.state.community.number_of_subscribers =
409         data.community.number_of_subscribers;
410       this.setState(this.state);
411     } else if (res.op == UserOperation.GetPosts) {
412       let data = res.data as GetPostsResponse;
413       this.state.posts = data.posts;
414       this.state.loading = false;
415       this.setState(this.state);
416       setupTippy();
417     } else if (
418       res.op == UserOperation.EditPost ||
419       res.op == UserOperation.DeletePost ||
420       res.op == UserOperation.RemovePost ||
421       res.op == UserOperation.LockPost ||
422       res.op == UserOperation.StickyPost
423     ) {
424       let data = res.data as PostResponse;
425       editPostFindRes(data, this.state.posts);
426       this.setState(this.state);
427     } else if (res.op == UserOperation.CreatePost) {
428       let data = res.data as PostResponse;
429       this.state.posts.unshift(data.post);
430       notifyPost(data.post, this.context.router);
431       this.setState(this.state);
432     } else if (res.op == UserOperation.CreatePostLike) {
433       let data = res.data as PostResponse;
434       createPostLikeFindRes(data, this.state.posts);
435       this.setState(this.state);
436     } else if (res.op == UserOperation.AddModToCommunity) {
437       let data = res.data as AddModToCommunityResponse;
438       this.state.moderators = data.moderators;
439       this.setState(this.state);
440     } else if (res.op == UserOperation.BanFromCommunity) {
441       let data = res.data as BanFromCommunityResponse;
442
443       this.state.posts
444         .filter(p => p.creator_id == data.user.id)
445         .forEach(p => (p.banned = data.banned));
446
447       this.setState(this.state);
448     } else if (res.op == UserOperation.GetComments) {
449       let data = res.data as GetCommentsResponse;
450       this.state.comments = data.comments;
451       this.state.loading = false;
452       this.setState(this.state);
453     } else if (
454       res.op == UserOperation.EditComment ||
455       res.op == UserOperation.DeleteComment ||
456       res.op == UserOperation.RemoveComment
457     ) {
458       let data = res.data as CommentResponse;
459       editCommentRes(data, this.state.comments);
460       this.setState(this.state);
461     } else if (res.op == UserOperation.CreateComment) {
462       let data = res.data as CommentResponse;
463
464       // Necessary since it might be a user reply
465       if (data.recipient_ids.length == 0) {
466         this.state.comments.unshift(data.comment);
467         this.setState(this.state);
468       }
469     } else if (res.op == UserOperation.SaveComment) {
470       let data = res.data as CommentResponse;
471       saveCommentRes(data, this.state.comments);
472       this.setState(this.state);
473     } else if (res.op == UserOperation.CreateCommentLike) {
474       let data = res.data as CommentResponse;
475       createCommentLikeRes(data, this.state.comments);
476       this.setState(this.state);
477     } else if (res.op == UserOperation.GetSite) {
478       let data = res.data as GetSiteResponse;
479       this.state.site = data.site;
480       this.state.admins = data.admins;
481       this.setState(this.state);
482     }
483   }
484 }