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