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