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