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