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