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