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