]> Untitled Git - lemmy.git/blob - ui/src/components/post-listing.tsx
Spanish translations
[lemmy.git] / ui / src / components / post-listing.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { WebSocketService, UserService } from '../services';
4 import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm, CommunityUser, UserView, BanType, BanFromCommunityForm, BanUserForm, AddModToCommunityForm, AddAdminForm, TransferSiteForm, TransferCommunityForm } from '../interfaces';
5 import { MomentTime } from './moment-time';
6 import { PostForm } from './post-form';
7 import { mdToHtml, canMod, isMod, isImage, isVideo, getUnixTime } from '../utils';
8 import { i18n } from '../i18next';
9 import { T } from 'inferno-i18next';
10
11 interface PostListingState {
12   showEdit: boolean;
13   showRemoveDialog: boolean;
14   removeReason: string;
15   showBanDialog: boolean;
16   banReason: string;
17   banExpires: string;
18   banType: BanType;
19   showConfirmTransferSite: boolean;
20   showConfirmTransferCommunity: boolean;
21   imageExpanded: boolean;
22   viewSource: boolean;
23 }
24
25 interface PostListingProps {
26   post: Post;
27   showCommunity?: boolean;
28   showBody?: boolean;
29   viewOnly?: boolean;
30   moderators?: Array<CommunityUser>;
31   admins?: Array<UserView>;
32 }
33
34 export class PostListing extends Component<PostListingProps, PostListingState> {
35
36   private emptyState: PostListingState = {
37     showEdit: false,
38     showRemoveDialog: false,
39     removeReason: null,
40     showBanDialog: false,
41     banReason: null,
42     banExpires: null,
43     banType: BanType.Community,
44     showConfirmTransferSite: false,
45     showConfirmTransferCommunity: false,
46     imageExpanded: false,
47     viewSource: false,
48   }
49
50   constructor(props: any, context: any) {
51     super(props, context);
52
53     this.state = this.emptyState;
54     this.handlePostLike = this.handlePostLike.bind(this);
55     this.handlePostDisLike = this.handlePostDisLike.bind(this);
56     this.handleEditPost = this.handleEditPost.bind(this);
57     this.handleEditCancel = this.handleEditCancel.bind(this);
58   }
59
60   render() {
61     return (
62       <div class="row">
63         {!this.state.showEdit 
64           ? this.listing()
65           : 
66           <div class="col-12">
67             <PostForm post={this.props.post} onEdit={this.handleEditPost} onCancel={this.handleEditCancel}/>
68           </div>
69         }
70       </div>
71     )
72   }
73
74   listing() {
75     let post = this.props.post;
76     return (
77       <div class="listing col-12">
78         <div className={`vote-bar mr-2 float-left small text-center ${this.props.viewOnly && 'no-click'}`}>
79           <button className={`btn p-0 ${post.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostLike)}>
80             <svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg>
81           </button>
82           <div class={`font-weight-bold text-muted`}>{post.score}</div>
83           <button className={`btn p-0 ${post.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostDisLike)}>
84             <svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg>
85           </button>
86         </div>
87         {post.url && isImage(post.url) &&
88           <span title={i18n.t('expand_here')} class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="mx-2 mt-1 float-left img-fluid thumbnail rounded" src={post.url} /></span>
89         }
90         {post.url && isVideo(post.url) &&
91           <video playsinline muted loop controls class="mx-2 mt-1 float-left" height="100" width="150">
92             <source src={post.url} type="video/mp4" />
93           </video>
94         }
95         <div className="ml-4">
96           <div className="post-title">
97             <h5 className="mb-0 d-inline">
98               {post.url ? 
99               <a className="text-body" href={post.url} target="_blank" title={post.url}>{post.name}</a> : 
100               <Link className="text-body" to={`/post/${post.id}`} title={i18n.t('comments')}>{post.name}</Link>
101               }
102             </h5>
103             {post.url && 
104               <small>
105                 <a className="ml-2 text-muted font-italic" href={post.url} target="_blank" title={post.url}>{(new URL(post.url)).hostname}</a>
106               </small>
107             }
108             { post.url && isImage(post.url) && 
109               <>
110                 { !this.state.imageExpanded
111                   ? <span class="text-monospace pointer ml-2 text-muted small" title={i18n.t('expand_here')} onClick={linkEvent(this, this.handleImageExpandClick)}>[+]</span>
112                   : 
113                   <span>
114                     <span class="text-monospace pointer ml-2 text-muted small" onClick={linkEvent(this, this.handleImageExpandClick)}>[-]</span>
115                     <div>
116                       <span class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="img-fluid" src={post.url} /></span>
117                     </div>
118                   </span>
119                 }
120               </>
121             }
122             {post.removed &&
123               <small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small>
124             }
125             {post.deleted &&
126               <small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small>
127             }
128             {post.locked &&
129               <small className="ml-2 text-muted font-italic"><T i18nKey="locked">#</T></small>
130             }
131             {post.stickied &&
132               <small className="ml-2 text-muted font-italic"><T i18nKey="stickied">#</T></small>
133             }
134             {post.nsfw &&
135               <small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small>
136             }
137           </div>
138         </div>
139         <div className="details ml-4">
140           <ul class="list-inline mb-0 text-muted small">
141             <li className="list-inline-item">
142               <span>{i18n.t('by')} </span>
143               <Link className="text-info" to={`/u/${post.creator_name}`}>{post.creator_name}</Link>
144               {this.isMod && 
145                 <span className="mx-1 badge badge-light"><T i18nKey="mod">#</T></span>
146               }
147               {this.isAdmin && 
148                 <span className="mx-1 badge badge-light"><T i18nKey="admin">#</T></span>
149               }
150               {(post.banned_from_community || post.banned) &&  
151                 <span className="mx-1 badge badge-danger"><T i18nKey="banned">#</T></span>
152               }
153               {this.props.showCommunity && 
154                 <span>
155                   <span> {i18n.t('to')} </span>
156                   <Link to={`/c/${post.community_name}`}>{post.community_name}</Link>
157                 </span>
158               }
159             </li>
160             <li className="list-inline-item">
161               <span><MomentTime data={post} /></span>
162             </li>
163             <li className="list-inline-item">
164               <span>(
165                 <span className="text-info">+{post.upvotes}</span>
166                 <span> | </span>
167                 <span className="text-danger">-{post.downvotes}</span>
168                 <span>) </span>
169               </span>
170             </li>
171             <li className="list-inline-item">
172               <Link className="text-muted" to={`/post/${post.id}`}><T i18nKey="number_of_comments" interpolation={{count: post.number_of_comments}}>#</T></Link>
173             </li>
174           </ul>
175           <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
176             {UserService.Instance.user &&
177               <>
178                 {this.props.showBody &&
179                   <>
180                     <li className="list-inline-item mr-2">
181                       <span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{post.saved ? i18n.t('unsave') : i18n.t('save')}</span>
182                     </li>
183                     <li className="list-inline-item mr-2">
184                       <Link className="text-muted" to={`/create_post${this.crossPostParams}`}><T i18nKey="cross_post">#</T></Link>
185                     </li>
186                   </>
187                 }
188                 {this.myPost && 
189                   <>
190                     <li className="list-inline-item">
191                       <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span>
192                     </li>
193                     <li className="list-inline-item mr-2">
194                       <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>
195                         {!post.deleted ? i18n.t('delete') : i18n.t('restore')}
196                       </span>
197                     </li>
198                   </>
199                 }
200                 {this.canModOnSelf &&
201                   <>
202                     <li className="list-inline-item">
203                       <span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{post.locked ? i18n.t('unlock') : i18n.t('lock')}</span>
204                     </li>
205                     <li className="list-inline-item">
206                       <span class="pointer" onClick={linkEvent(this, this.handleModSticky)}>{post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}</span>
207                     </li>
208                   </>
209                 }
210                 {/* Mods can ban from community, and appoint as mods to community */}
211                 {(this.canMod || this.canAdmin) &&
212                   <li className="list-inline-item">
213                     {!post.removed ? 
214                     <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> :
215                     <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span>
216                     }
217                   </li>
218                 }
219                 {this.canMod && 
220                   <>
221                     {!this.isMod && 
222                       <li className="list-inline-item">
223                         {!post.banned_from_community ? 
224                         <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}><T i18nKey="ban">#</T></span> :
225                         <span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}><T i18nKey="unban">#</T></span>
226                         }
227                       </li>
228                     }
229                     {!post.banned_from_community &&
230                       <li className="list-inline-item">
231                         <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')}</span>
232                       </li>
233                     }
234                   </>
235                 }
236                 {/* Community creators and admins can transfer community to another mod */}
237                 {(this.amCommunityCreator || this.canAdmin) && this.isMod &&
238                   <li className="list-inline-item">
239                     {!this.state.showConfirmTransferCommunity ?
240                     <span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}><T i18nKey="transfer_community">#</T>
241                   </span> : <>
242                     <span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
243                     <span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="yes">#</T></span>
244                     <span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferCommunity)}><T i18nKey="no">#</T></span>
245                   </>
246                     }
247                   </li>
248                 }
249                 {/* Admins can ban from all, and appoint other admins */}
250                 {this.canAdmin &&
251                   <>
252                     {!this.isAdmin && 
253                       <li className="list-inline-item">
254                         {!post.banned ? 
255                         <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}><T i18nKey="ban_from_site">#</T></span> :
256                         <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}><T i18nKey="unban_from_site">#</T></span>
257                         }
258                       </li>
259                     }
260                     {!post.banned &&
261                       <li className="list-inline-item">
262                         <span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')}</span>
263                       </li>
264                     }
265                   </>
266                 }
267                 {/* Site Creator can transfer to another admin */}
268                 {this.amSiteCreator && this.isAdmin &&
269                   <li className="list-inline-item">
270                     {!this.state.showConfirmTransferSite ?
271                     <span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferSite)}><T i18nKey="transfer_site">#</T>
272                   </span> : <>
273                     <span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span>
274                     <span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="yes">#</T></span>
275                     <span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferSite)}><T i18nKey="no">#</T></span>
276                   </>
277                     }
278                   </li>
279                 }
280               </>
281             }
282             {this.props.showBody && post.body && 
283               <li className="list-inline-item">
284                 <span className="pointer" onClick={linkEvent(this, this.handleViewSource)}><T i18nKey="view_source">#</T></span>
285               </li>
286             }
287           </ul>
288           {this.state.showRemoveDialog && 
289             <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
290               <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
291               <button type="submit" class="btn btn-secondary"><T i18nKey="remove_post">#</T></button>
292             </form>
293           }
294           {this.state.showBanDialog && 
295             <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
296               <div class="form-group row">
297                 <label class="col-form-label"><T i18nKey="reason">#</T></label>
298                 <input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
299               </div>
300               {/* TODO hold off on expires until later */}
301               {/* <div class="form-group row"> */}
302                 {/*   <label class="col-form-label">Expires</label> */}
303                 {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
304                 {/* </div> */}
305                 <div class="form-group row">
306                   <button type="submit" class="btn btn-secondary">{i18n.t('ban')} {post.creator_name}</button>
307                 </div>
308               </form>
309           }
310             {this.props.showBody && post.body &&
311               <>
312                 {this.state.viewSource ? <pre>{post.body}</pre> : 
313                 <div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />
314                 }
315               </>
316             }
317         </div>
318       </div>
319     )
320   }
321
322   private get myPost(): boolean {
323     return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id;
324   }
325
326   get isMod(): boolean {
327     return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.post.creator_id);
328   }
329
330   get isAdmin(): boolean {
331     return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.post.creator_id);
332   }
333
334   get canMod(): boolean {
335     if (this.props.admins && this.props.moderators) {
336       let adminsThenMods = this.props.admins.map(a => a.id)
337       .concat(this.props.moderators.map(m => m.user_id));
338
339       return canMod(UserService.Instance.user, adminsThenMods, this.props.post.creator_id);
340       } else { 
341       return false;
342     }
343   }
344
345   get canModOnSelf(): boolean {
346     if (this.props.admins && this.props.moderators) {
347       let adminsThenMods = this.props.admins.map(a => a.id)
348       .concat(this.props.moderators.map(m => m.user_id));
349
350       return canMod(UserService.Instance.user, adminsThenMods, this.props.post.creator_id, true);
351     } else { 
352       return false;
353     }
354   }
355
356   get canAdmin(): boolean {
357     return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.post.creator_id);
358   }
359
360   get amCommunityCreator(): boolean {
361     return this.props.moderators && 
362       UserService.Instance.user && 
363       (this.props.post.creator_id != UserService.Instance.user.id) &&
364       (UserService.Instance.user.id == this.props.moderators[0].user_id);
365   }
366
367   get amSiteCreator(): boolean {
368     return this.props.admins && 
369       UserService.Instance.user && 
370       (this.props.post.creator_id != UserService.Instance.user.id) &&
371       (UserService.Instance.user.id == this.props.admins[0].id);
372   }
373
374   handlePostLike(i: PostListing) {
375
376     let form: CreatePostLikeForm = {
377       post_id: i.props.post.id,
378       score: (i.props.post.my_vote == 1) ? 0 : 1
379     };
380     WebSocketService.Instance.likePost(form);
381   }
382
383   handlePostDisLike(i: PostListing) {
384     let form: CreatePostLikeForm = {
385       post_id: i.props.post.id,
386       score: (i.props.post.my_vote == -1) ? 0 : -1
387     };
388     WebSocketService.Instance.likePost(form);
389   }
390
391   handleEditClick(i: PostListing) {
392     i.state.showEdit = true;
393     i.setState(i.state);
394   }
395
396   handleEditCancel() {
397     this.state.showEdit = false;
398     this.setState(this.state);
399   }
400
401   // The actual editing is done in the recieve for post
402   handleEditPost() {
403     this.state.showEdit = false;
404     this.setState(this.state);
405   }
406
407   handleDeleteClick(i: PostListing) {
408     let deleteForm: PostFormI = {
409       body: i.props.post.body,
410       community_id: i.props.post.community_id,
411       name: i.props.post.name,
412       url: i.props.post.url,
413       edit_id: i.props.post.id,
414       creator_id: i.props.post.creator_id,
415       deleted: !i.props.post.deleted,
416       nsfw: i.props.post.nsfw,
417       auth: null
418     };
419     WebSocketService.Instance.editPost(deleteForm);
420   }
421
422   handleSavePostClick(i: PostListing) {
423     let saved = (i.props.post.saved == undefined) ? true : !i.props.post.saved;
424     let form: SavePostForm = {
425       post_id: i.props.post.id,
426       save: saved
427     };
428
429     WebSocketService.Instance.savePost(form);
430   }
431
432   get crossPostParams(): string {
433     let params = `?name=${this.props.post.name}`;
434     if (this.props.post.url) {
435       params += `&url=${this.props.post.url}`;
436     }
437     if (this.props.post.body) {
438       params += `&body=${this.props.post.body}`;
439     }
440     return params;
441   }
442
443   handleModRemoveShow(i: PostListing) {
444     i.state.showRemoveDialog = true;
445     i.setState(i.state);
446   }
447
448   handleModRemoveReasonChange(i: PostListing, event: any) {
449     i.state.removeReason = event.target.value;
450     i.setState(i.state);
451   }
452
453   handleModRemoveSubmit(i: PostListing) {
454     event.preventDefault();
455     let form: PostFormI = {
456       name: i.props.post.name,
457       community_id: i.props.post.community_id,
458       edit_id: i.props.post.id,
459       creator_id: i.props.post.creator_id,
460       removed: !i.props.post.removed,
461       reason: i.state.removeReason,
462       nsfw: i.props.post.nsfw,
463       auth: null,
464     };
465     WebSocketService.Instance.editPost(form);
466
467     i.state.showRemoveDialog = false;
468     i.setState(i.state);
469   }
470
471   handleModLock(i: PostListing) {
472     let form: PostFormI = {
473       name: i.props.post.name,
474       community_id: i.props.post.community_id,
475       edit_id: i.props.post.id,
476       creator_id: i.props.post.creator_id,
477       nsfw: i.props.post.nsfw,
478       locked: !i.props.post.locked,
479       auth: null,
480     };
481     WebSocketService.Instance.editPost(form);
482   }
483
484   handleModSticky(i: PostListing) {
485     let form: PostFormI = {
486       name: i.props.post.name,
487       community_id: i.props.post.community_id,
488       edit_id: i.props.post.id,
489       creator_id: i.props.post.creator_id,
490       nsfw: i.props.post.nsfw,
491       stickied: !i.props.post.stickied,
492       auth: null,
493     };
494     WebSocketService.Instance.editPost(form);
495   }
496
497   handleModBanFromCommunityShow(i: PostListing) {
498     i.state.showBanDialog = true;
499     i.state.banType = BanType.Community;
500     i.setState(i.state);
501   }
502
503   handleModBanShow(i: PostListing) {
504     i.state.showBanDialog = true;
505     i.state.banType = BanType.Site;
506     i.setState(i.state);
507   }
508
509   handleModBanReasonChange(i: PostListing, event: any) {
510     i.state.banReason = event.target.value;
511     i.setState(i.state);
512   }
513
514   handleModBanExpiresChange(i: PostListing, event: any) {
515     i.state.banExpires = event.target.value;
516     i.setState(i.state);
517   }
518
519   handleModBanFromCommunitySubmit(i: PostListing) {
520     i.state.banType = BanType.Community;
521     i.setState(i.state);
522     i.handleModBanBothSubmit(i);
523   }
524
525   handleModBanSubmit(i: PostListing) {
526     i.state.banType = BanType.Site;
527     i.setState(i.state);
528     i.handleModBanBothSubmit(i);
529   }
530
531   handleModBanBothSubmit(i: PostListing) {
532     event.preventDefault();
533
534     if (i.state.banType == BanType.Community) {
535       let form: BanFromCommunityForm = {
536         user_id: i.props.post.creator_id,
537         community_id: i.props.post.community_id,
538         ban: !i.props.post.banned_from_community,
539         reason: i.state.banReason,
540         expires: getUnixTime(i.state.banExpires),
541       };
542       WebSocketService.Instance.banFromCommunity(form);
543     } else {
544       let form: BanUserForm = {
545         user_id: i.props.post.creator_id,
546         ban: !i.props.post.banned,
547         reason: i.state.banReason,
548         expires: getUnixTime(i.state.banExpires),
549       };
550       WebSocketService.Instance.banUser(form);
551     }
552
553     i.state.showBanDialog = false;
554     i.setState(i.state);
555   }
556
557   handleAddModToCommunity(i: PostListing) {
558     let form: AddModToCommunityForm = {
559       user_id: i.props.post.creator_id,
560       community_id: i.props.post.community_id,
561       added: !i.isMod,
562     };
563     WebSocketService.Instance.addModToCommunity(form);
564     i.setState(i.state);
565   }
566
567   handleAddAdmin(i: PostListing) {
568     let form: AddAdminForm = {
569       user_id: i.props.post.creator_id,
570       added: !i.isAdmin,
571     };
572     WebSocketService.Instance.addAdmin(form);
573     i.setState(i.state);
574   }
575
576   handleShowConfirmTransferCommunity(i: PostListing) { 
577     i.state.showConfirmTransferCommunity = true;
578     i.setState(i.state);
579   }
580
581   handleCancelShowConfirmTransferCommunity(i: PostListing) { 
582     i.state.showConfirmTransferCommunity = false;
583     i.setState(i.state);
584   }
585
586   handleTransferCommunity(i: PostListing) {
587     let form: TransferCommunityForm = {
588       community_id: i.props.post.community_id,
589       user_id: i.props.post.creator_id,
590     };
591     WebSocketService.Instance.transferCommunity(form);
592     i.state.showConfirmTransferCommunity = false;
593     i.setState(i.state);
594   }
595
596   handleShowConfirmTransferSite(i: PostListing) { 
597     i.state.showConfirmTransferSite = true;
598     i.setState(i.state);
599   }
600
601   handleCancelShowConfirmTransferSite(i: PostListing) { 
602     i.state.showConfirmTransferSite = false;
603     i.setState(i.state);
604   }
605
606   handleTransferSite(i: PostListing) {
607     let form: TransferSiteForm = {
608       user_id: i.props.post.creator_id,
609     };
610     WebSocketService.Instance.transferSite(form);
611     i.state.showConfirmTransferSite = false;
612     i.setState(i.state);
613   }
614
615   handleImageExpandClick(i: PostListing) {
616     i.state.imageExpanded = !i.state.imageExpanded;
617     i.setState(i.state);
618   }
619
620   handleViewSource(i: PostListing) {
621     i.state.viewSource = !i.state.viewSource;
622     i.setState(i.state);
623   }
624 }
625