]> Untitled Git - lemmy.git/blob - ui/src/components/community.tsx
Merge remote-tracking branch 'weblate/master'
[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   // Necessary for back button for some reason
147   componentWillReceiveProps(nextProps: any) {
148     if (
149       nextProps.history.action == 'POP' ||
150       nextProps.history.action == 'PUSH'
151     ) {
152       this.state.dataType = getDataTypeFromProps(nextProps);
153       this.state.sort = getSortTypeFromProps(nextProps);
154       this.state.page = getPageFromProps(nextProps);
155       this.setState(this.state);
156       this.fetchData();
157     }
158   }
159
160   render() {
161     return (
162       <div class="container">
163         {this.selects()}
164         {this.state.loading ? (
165           <h5>
166             <svg class="icon icon-spinner spin">
167               <use xlinkHref="#icon-spinner"></use>
168             </svg>
169           </h5>
170         ) : (
171           <div class="row">
172             <div class="col-12 col-md-8">
173               <h5>
174                 {this.state.community.title}
175                 {this.state.community.removed && (
176                   <small className="ml-2 text-muted font-italic">
177                     {i18n.t('removed')}
178                   </small>
179                 )}
180                 {this.state.community.nsfw && (
181                   <small className="ml-2 text-muted font-italic">
182                     {i18n.t('nsfw')}
183                   </small>
184                 )}
185               </h5>
186               {this.listings()}
187               {this.paginator()}
188             </div>
189             <div class="col-12 col-md-4">
190               <Sidebar
191                 community={this.state.community}
192                 moderators={this.state.moderators}
193                 admins={this.state.admins}
194                 online={this.state.online}
195                 enableNsfw={this.state.site.enable_nsfw}
196               />
197             </div>
198           </div>
199         )}
200       </div>
201     );
202   }
203
204   listings() {
205     return this.state.dataType == DataType.Post ? (
206       <PostListings
207         posts={this.state.posts}
208         removeDuplicates
209         sort={this.state.sort}
210         enableDownvotes={this.state.site.enable_downvotes}
211         enableNsfw={this.state.site.enable_nsfw}
212       />
213     ) : (
214       <CommentNodes
215         nodes={commentsToFlatNodes(this.state.comments)}
216         noIndent
217         sortType={this.state.sort}
218         showContext
219         enableDownvotes={this.state.site.enable_downvotes}
220       />
221     );
222   }
223
224   selects() {
225     return (
226       <div class="mb-3">
227         <span class="mr-3">
228           <DataTypeSelect
229             type_={this.state.dataType}
230             onChange={this.handleDataTypeChange}
231           />
232         </span>
233         <span class="mr-2">
234           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
235         </span>
236         <a
237           href={`/feeds/c/${this.state.communityName}.xml?sort=${
238             SortType[this.state.sort]
239           }`}
240           target="_blank"
241           title="RSS"
242           rel="noopener"
243         >
244           <svg class="icon text-muted small">
245             <use xlinkHref="#icon-rss">#</use>
246           </svg>
247         </a>
248       </div>
249     );
250   }
251
252   paginator() {
253     return (
254       <div class="my-2">
255         {this.state.page > 1 && (
256           <button
257             class="btn btn-sm btn-secondary mr-1"
258             onClick={linkEvent(this, this.prevPage)}
259           >
260             {i18n.t('prev')}
261           </button>
262         )}
263         {this.state.posts.length > 0 && (
264           <button
265             class="btn btn-sm btn-secondary"
266             onClick={linkEvent(this, this.nextPage)}
267           >
268             {i18n.t('next')}
269           </button>
270         )}
271       </div>
272     );
273   }
274
275   nextPage(i: Community) {
276     i.state.page++;
277     i.setState(i.state);
278     i.updateUrl();
279     i.fetchData();
280     window.scrollTo(0, 0);
281   }
282
283   prevPage(i: Community) {
284     i.state.page--;
285     i.setState(i.state);
286     i.updateUrl();
287     i.fetchData();
288     window.scrollTo(0, 0);
289   }
290
291   handleSortChange(val: SortType) {
292     this.state.sort = val;
293     this.state.page = 1;
294     this.state.loading = true;
295     this.setState(this.state);
296     this.updateUrl();
297     this.fetchData();
298     window.scrollTo(0, 0);
299   }
300
301   handleDataTypeChange(val: DataType) {
302     this.state.dataType = val;
303     this.state.page = 1;
304     this.state.loading = true;
305     this.setState(this.state);
306     this.updateUrl();
307     this.fetchData();
308     window.scrollTo(0, 0);
309   }
310
311   updateUrl() {
312     let dataTypeStr = DataType[this.state.dataType].toLowerCase();
313     let sortStr = SortType[this.state.sort].toLowerCase();
314     this.props.history.push(
315       `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${this.state.page}`
316     );
317   }
318
319   fetchData() {
320     if (this.state.dataType == DataType.Post) {
321       let getPostsForm: GetPostsForm = {
322         page: this.state.page,
323         limit: fetchLimit,
324         sort: SortType[this.state.sort],
325         type_: ListingType[ListingType.Community],
326         community_id: this.state.community.id,
327       };
328       WebSocketService.Instance.getPosts(getPostsForm);
329     } else {
330       let getCommentsForm: GetCommentsForm = {
331         page: this.state.page,
332         limit: fetchLimit,
333         sort: SortType[this.state.sort],
334         type_: ListingType[ListingType.Community],
335         community_id: this.state.community.id,
336       };
337       WebSocketService.Instance.getComments(getCommentsForm);
338     }
339   }
340
341   parseMessage(msg: WebSocketJsonResponse) {
342     console.log(msg);
343     let res = wsJsonToRes(msg);
344     if (msg.error) {
345       toast(i18n.t(msg.error), 'danger');
346       this.context.router.history.push('/');
347       return;
348     } else if (msg.reconnect) {
349       this.fetchData();
350     } else if (res.op == UserOperation.GetCommunity) {
351       let data = res.data as GetCommunityResponse;
352       this.state.community = data.community;
353       this.state.moderators = data.moderators;
354       this.state.admins = data.admins;
355       this.state.online = data.online;
356       document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
357       this.setState(this.state);
358       this.fetchData();
359     } else if (res.op == UserOperation.EditCommunity) {
360       let data = res.data as CommunityResponse;
361       this.state.community = data.community;
362       this.setState(this.state);
363     } else if (res.op == UserOperation.FollowCommunity) {
364       let data = res.data as CommunityResponse;
365       this.state.community.subscribed = data.community.subscribed;
366       this.state.community.number_of_subscribers =
367         data.community.number_of_subscribers;
368       this.setState(this.state);
369     } else if (res.op == UserOperation.GetPosts) {
370       let data = res.data as GetPostsResponse;
371       this.state.posts = data.posts;
372       this.state.loading = false;
373       this.setState(this.state);
374       setupTippy();
375     } else if (res.op == UserOperation.EditPost) {
376       let data = res.data as PostResponse;
377       editPostFindRes(data, this.state.posts);
378       this.setState(this.state);
379     } else if (res.op == UserOperation.CreatePost) {
380       let data = res.data as PostResponse;
381       this.state.posts.unshift(data.post);
382       this.setState(this.state);
383     } else if (res.op == UserOperation.CreatePostLike) {
384       let data = res.data as PostResponse;
385       createPostLikeFindRes(data, this.state.posts);
386       this.setState(this.state);
387     } else if (res.op == UserOperation.AddModToCommunity) {
388       let data = res.data as AddModToCommunityResponse;
389       this.state.moderators = data.moderators;
390       this.setState(this.state);
391     } else if (res.op == UserOperation.BanFromCommunity) {
392       let data = res.data as BanFromCommunityResponse;
393
394       this.state.posts
395         .filter(p => p.creator_id == data.user.id)
396         .forEach(p => (p.banned = data.banned));
397
398       this.setState(this.state);
399     } else if (res.op == UserOperation.GetComments) {
400       let data = res.data as GetCommentsResponse;
401       this.state.comments = data.comments;
402       this.state.loading = false;
403       this.setState(this.state);
404     } else if (res.op == UserOperation.EditComment) {
405       let data = res.data as CommentResponse;
406       editCommentRes(data, this.state.comments);
407       this.setState(this.state);
408     } else if (res.op == UserOperation.CreateComment) {
409       let data = res.data as CommentResponse;
410
411       // Necessary since it might be a user reply
412       if (data.recipient_ids.length == 0) {
413         this.state.comments.unshift(data.comment);
414         this.setState(this.state);
415       }
416     } else if (res.op == UserOperation.SaveComment) {
417       let data = res.data as CommentResponse;
418       saveCommentRes(data, this.state.comments);
419       this.setState(this.state);
420     } else if (res.op == UserOperation.CreateCommentLike) {
421       let data = res.data as CommentResponse;
422       createCommentLikeRes(data, this.state.comments);
423       this.setState(this.state);
424     } else if (res.op == UserOperation.GetSite) {
425       let data = res.data as GetSiteResponse;
426       this.state.site = data.site;
427       this.setState(this.state);
428     }
429   }
430 }