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