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