]> Untitled Git - lemmy.git/blob - ui/src/components/post.tsx
Finishing up interpolation rework.
[lemmy.git] / ui / src / components / post.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,
7   Post as PostI,
8   GetPostResponse,
9   PostResponse,
10   Comment,
11   CommentForm as CommentFormI,
12   CommentResponse,
13   CommentSortType,
14   CommunityUser,
15   CommunityResponse,
16   CommentNode as CommentNodeI,
17   BanFromCommunityResponse,
18   BanUserResponse,
19   AddModToCommunityResponse,
20   AddAdminResponse,
21   UserView,
22   SearchType,
23   SortType,
24   SearchForm,
25   GetPostForm,
26   SearchResponse,
27   GetSiteResponse,
28   GetCommunityResponse,
29   WebSocketJsonResponse,
30 } from '../interfaces';
31 import { WebSocketService, UserService } from '../services';
32 import { wsJsonToRes, hotRank, toast } from '../utils';
33 import { PostListing } from './post-listing';
34 import { PostListings } from './post-listings';
35 import { Sidebar } from './sidebar';
36 import { CommentForm } from './comment-form';
37 import { CommentNodes } from './comment-nodes';
38 import autosize from 'autosize';
39 import { i18n } from '../i18next';
40
41 interface PostState {
42   post: PostI;
43   comments: Array<Comment>;
44   commentSort: CommentSortType;
45   community: Community;
46   moderators: Array<CommunityUser>;
47   admins: Array<UserView>;
48   scrolled?: boolean;
49   scrolled_comment_id?: number;
50   loading: boolean;
51   crossPosts: Array<PostI>;
52 }
53
54 export class Post extends Component<any, PostState> {
55   private subscription: Subscription;
56   private emptyState: PostState = {
57     post: null,
58     comments: [],
59     commentSort: CommentSortType.Hot,
60     community: null,
61     moderators: [],
62     admins: [],
63     scrolled: false,
64     loading: true,
65     crossPosts: [],
66   };
67
68   constructor(props: any, context: any) {
69     super(props, context);
70
71     this.state = this.emptyState;
72
73     let postId = Number(this.props.match.params.id);
74     if (this.props.match.params.comment_id) {
75       this.state.scrolled_comment_id = this.props.match.params.comment_id;
76     }
77
78     this.subscription = WebSocketService.Instance.subject
79       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
80       .subscribe(
81         msg => this.parseMessage(msg),
82         err => console.error(err),
83         () => console.log('complete')
84       );
85
86     let form: GetPostForm = {
87       id: postId,
88     };
89     WebSocketService.Instance.getPost(form);
90   }
91
92   componentWillUnmount() {
93     this.subscription.unsubscribe();
94   }
95
96   componentDidMount() {
97     autosize(document.querySelectorAll('textarea'));
98   }
99
100   componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
101     if (
102       this.state.scrolled_comment_id &&
103       !this.state.scrolled &&
104       lastState.comments.length > 0
105     ) {
106       var elmnt = document.getElementById(
107         `comment-${this.state.scrolled_comment_id}`
108       );
109       elmnt.scrollIntoView();
110       elmnt.classList.add('mark');
111       this.state.scrolled = true;
112       this.markScrolledAsRead(this.state.scrolled_comment_id);
113     }
114
115     // Necessary if you are on a post and you click another post (same route)
116     if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
117       // Couldnt get a refresh working. This does for now.
118       location.reload();
119
120       // let currentId = this.props.match.params.id;
121       // WebSocketService.Instance.getPost(currentId);
122       // this.context.router.history.push('/sponsors');
123       // this.context.refresh();
124       // this.context.router.history.push(_lastProps.location.pathname);
125     }
126   }
127
128   markScrolledAsRead(commentId: number) {
129     let found = this.state.comments.find(c => c.id == commentId);
130     let parent = this.state.comments.find(c => found.parent_id == c.id);
131     let parent_user_id = parent
132       ? parent.creator_id
133       : this.state.post.creator_id;
134
135     if (
136       UserService.Instance.user &&
137       UserService.Instance.user.id == parent_user_id
138     ) {
139       let form: CommentFormI = {
140         content: found.content,
141         edit_id: found.id,
142         creator_id: found.creator_id,
143         post_id: found.post_id,
144         parent_id: found.parent_id,
145         read: true,
146         auth: null,
147       };
148       WebSocketService.Instance.editComment(form);
149     }
150   }
151
152   render() {
153     return (
154       <div class="container">
155         {this.state.loading ? (
156           <h5>
157             <svg class="icon icon-spinner spin">
158               <use xlinkHref="#icon-spinner"></use>
159             </svg>
160           </h5>
161         ) : (
162           <div class="row">
163             <div class="col-12 col-md-8 mb-3">
164               <PostListing
165                 post={this.state.post}
166                 showBody
167                 showCommunity
168                 moderators={this.state.moderators}
169                 admins={this.state.admins}
170               />
171               {this.state.crossPosts.length > 0 && (
172                 <>
173                   <div class="my-1 text-muted small font-weight-bold">
174                     {i18n.t('cross_posts')}
175                   </div>
176                   <PostListings showCommunity posts={this.state.crossPosts} />
177                 </>
178               )}
179               <div className="mb-2" />
180               <CommentForm
181                 postId={this.state.post.id}
182                 disabled={this.state.post.locked}
183               />
184               {this.state.comments.length > 0 && this.sortRadios()}
185               {this.commentsTree()}
186             </div>
187             <div class="col-12 col-sm-12 col-md-4">
188               {this.state.comments.length > 0 && this.newComments()}
189               {this.sidebar()}
190             </div>
191           </div>
192         )}
193       </div>
194     );
195   }
196
197   sortRadios() {
198     return (
199       <div class="btn-group btn-group-toggle mb-3">
200         <label
201           className={`btn btn-sm btn-secondary pointer ${this.state
202             .commentSort === CommentSortType.Hot && 'active'}`}
203         >
204           {i18n.t('hot')}
205           <input
206             type="radio"
207             value={CommentSortType.Hot}
208             checked={this.state.commentSort === CommentSortType.Hot}
209             onChange={linkEvent(this, this.handleCommentSortChange)}
210           />
211         </label>
212         <label
213           className={`btn btn-sm btn-secondary pointer ${this.state
214             .commentSort === CommentSortType.Top && 'active'}`}
215         >
216           {i18n.t('top')}
217           <input
218             type="radio"
219             value={CommentSortType.Top}
220             checked={this.state.commentSort === CommentSortType.Top}
221             onChange={linkEvent(this, this.handleCommentSortChange)}
222           />
223         </label>
224         <label
225           className={`btn btn-sm btn-secondary pointer ${this.state
226             .commentSort === CommentSortType.New && 'active'}`}
227         >
228           {i18n.t('new')}
229           <input
230             type="radio"
231             value={CommentSortType.New}
232             checked={this.state.commentSort === CommentSortType.New}
233             onChange={linkEvent(this, this.handleCommentSortChange)}
234           />
235         </label>
236       </div>
237     );
238   }
239
240   newComments() {
241     return (
242       <div class="d-none d-md-block new-comments mb-3 card border-secondary">
243         <div class="card-body small">
244           <h6>{i18n.t('recent_comments')}</h6>
245           {this.state.comments.map(comment => (
246             <CommentNodes
247               nodes={[{ comment: comment }]}
248               noIndent
249               locked={this.state.post.locked}
250               moderators={this.state.moderators}
251               admins={this.state.admins}
252               postCreatorId={this.state.post.creator_id}
253             />
254           ))}
255         </div>
256       </div>
257     );
258   }
259
260   sidebar() {
261     return (
262       <div class="mb-3">
263         <Sidebar
264           community={this.state.community}
265           moderators={this.state.moderators}
266           admins={this.state.admins}
267         />
268       </div>
269     );
270   }
271
272   handleCommentSortChange(i: Post, event: any) {
273     i.state.commentSort = Number(event.target.value);
274     i.setState(i.state);
275   }
276
277   private buildCommentsTree(): Array<CommentNodeI> {
278     let map = new Map<number, CommentNodeI>();
279     for (let comment of this.state.comments) {
280       let node: CommentNodeI = {
281         comment: comment,
282         children: [],
283       };
284       map.set(comment.id, { ...node });
285     }
286     let tree: Array<CommentNodeI> = [];
287     for (let comment of this.state.comments) {
288       if (comment.parent_id) {
289         map.get(comment.parent_id).children.push(map.get(comment.id));
290       } else {
291         tree.push(map.get(comment.id));
292       }
293     }
294
295     this.sortTree(tree);
296
297     return tree;
298   }
299
300   sortTree(tree: Array<CommentNodeI>) {
301     // First, put removed and deleted comments at the bottom, then do your other sorts
302     if (this.state.commentSort == CommentSortType.Top) {
303       tree.sort(
304         (a, b) =>
305           +a.comment.removed - +b.comment.removed ||
306           +a.comment.deleted - +b.comment.deleted ||
307           b.comment.score - a.comment.score
308       );
309     } else if (this.state.commentSort == CommentSortType.New) {
310       tree.sort(
311         (a, b) =>
312           +a.comment.removed - +b.comment.removed ||
313           +a.comment.deleted - +b.comment.deleted ||
314           b.comment.published.localeCompare(a.comment.published)
315       );
316     } else if (this.state.commentSort == CommentSortType.Hot) {
317       tree.sort(
318         (a, b) =>
319           +a.comment.removed - +b.comment.removed ||
320           +a.comment.deleted - +b.comment.deleted ||
321           hotRank(b.comment) - hotRank(a.comment)
322       );
323     }
324
325     for (let node of tree) {
326       this.sortTree(node.children);
327     }
328   }
329
330   commentsTree() {
331     let nodes = this.buildCommentsTree();
332     return (
333       <div>
334         <CommentNodes
335           nodes={nodes}
336           locked={this.state.post.locked}
337           moderators={this.state.moderators}
338           admins={this.state.admins}
339           postCreatorId={this.state.post.creator_id}
340         />
341       </div>
342     );
343   }
344
345   parseMessage(msg: WebSocketJsonResponse) {
346     console.log(msg);
347     let res = wsJsonToRes(msg);
348     if (msg.error) {
349       toast(i18n.t(msg.error), 'danger');
350       return;
351     } else if (res.op == UserOperation.GetPost) {
352       let data = res.data as GetPostResponse;
353       this.state.post = data.post;
354       this.state.comments = data.comments;
355       this.state.community = data.community;
356       this.state.moderators = data.moderators;
357       this.state.admins = data.admins;
358       this.state.loading = false;
359       document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
360
361       // Get cross-posts
362       if (this.state.post.url) {
363         let form: SearchForm = {
364           q: this.state.post.url,
365           type_: SearchType[SearchType.Url],
366           sort: SortType[SortType.TopAll],
367           page: 1,
368           limit: 6,
369         };
370         WebSocketService.Instance.search(form);
371       }
372
373       this.setState(this.state);
374     } else if (res.op == UserOperation.CreateComment) {
375       let data = res.data as CommentResponse;
376       this.state.comments.unshift(data.comment);
377       this.setState(this.state);
378     } else if (res.op == UserOperation.EditComment) {
379       let data = res.data as CommentResponse;
380       let found = this.state.comments.find(c => c.id == data.comment.id);
381       found.content = data.comment.content;
382       found.updated = data.comment.updated;
383       found.removed = data.comment.removed;
384       found.deleted = data.comment.deleted;
385       found.upvotes = data.comment.upvotes;
386       found.downvotes = data.comment.downvotes;
387       found.score = data.comment.score;
388       found.read = data.comment.read;
389
390       this.setState(this.state);
391     } else if (res.op == UserOperation.SaveComment) {
392       let data = res.data as CommentResponse;
393       let found = this.state.comments.find(c => c.id == data.comment.id);
394       found.saved = data.comment.saved;
395       this.setState(this.state);
396     } else if (res.op == UserOperation.CreateCommentLike) {
397       let data = res.data as CommentResponse;
398       let found: Comment = this.state.comments.find(
399         c => c.id === data.comment.id
400       );
401       found.score = data.comment.score;
402       found.upvotes = data.comment.upvotes;
403       found.downvotes = data.comment.downvotes;
404       if (data.comment.my_vote !== null) {
405         found.my_vote = data.comment.my_vote;
406         found.upvoteLoading = false;
407         found.downvoteLoading = false;
408       }
409       this.setState(this.state);
410     } else if (res.op == UserOperation.CreatePostLike) {
411       let data = res.data as PostResponse;
412       this.state.post.my_vote = data.post.my_vote;
413       this.state.post.score = data.post.score;
414       this.state.post.upvotes = data.post.upvotes;
415       this.state.post.downvotes = data.post.downvotes;
416       this.setState(this.state);
417     } else if (res.op == UserOperation.EditPost) {
418       let data = res.data as PostResponse;
419       this.state.post = data.post;
420       this.setState(this.state);
421     } else if (res.op == UserOperation.SavePost) {
422       let data = res.data as PostResponse;
423       this.state.post = data.post;
424       this.setState(this.state);
425     } else if (res.op == UserOperation.EditCommunity) {
426       let data = res.data as CommunityResponse;
427       this.state.community = data.community;
428       this.state.post.community_id = data.community.id;
429       this.state.post.community_name = data.community.name;
430       this.setState(this.state);
431     } else if (res.op == UserOperation.FollowCommunity) {
432       let data = res.data as CommunityResponse;
433       this.state.community.subscribed = data.community.subscribed;
434       this.state.community.number_of_subscribers =
435         data.community.number_of_subscribers;
436       this.setState(this.state);
437     } else if (res.op == UserOperation.BanFromCommunity) {
438       let data = res.data as BanFromCommunityResponse;
439       this.state.comments
440         .filter(c => c.creator_id == data.user.id)
441         .forEach(c => (c.banned_from_community = data.banned));
442       if (this.state.post.creator_id == data.user.id) {
443         this.state.post.banned_from_community = data.banned;
444       }
445       this.setState(this.state);
446     } else if (res.op == UserOperation.AddModToCommunity) {
447       let data = res.data as AddModToCommunityResponse;
448       this.state.moderators = data.moderators;
449       this.setState(this.state);
450     } else if (res.op == UserOperation.BanUser) {
451       let data = res.data as BanUserResponse;
452       this.state.comments
453         .filter(c => c.creator_id == data.user.id)
454         .forEach(c => (c.banned = data.banned));
455       if (this.state.post.creator_id == data.user.id) {
456         this.state.post.banned = data.banned;
457       }
458       this.setState(this.state);
459     } else if (res.op == UserOperation.AddAdmin) {
460       let data = res.data as AddAdminResponse;
461       this.state.admins = data.admins;
462       this.setState(this.state);
463     } else if (res.op == UserOperation.Search) {
464       let data = res.data as SearchResponse;
465       this.state.crossPosts = data.posts.filter(
466         p => p.id != this.state.post.id
467       );
468       this.setState(this.state);
469     } else if (res.op == UserOperation.TransferSite) {
470       let data = res.data as GetSiteResponse;
471
472       this.state.admins = data.admins;
473       this.setState(this.state);
474     } else if (res.op == UserOperation.TransferCommunity) {
475       let data = res.data as GetCommunityResponse;
476       this.state.community = data.community;
477       this.state.moderators = data.moderators;
478       this.state.admins = data.admins;
479       this.setState(this.state);
480     }
481   }
482 }