]> Untitled Git - lemmy-ui.git/blob - src/shared/components/post/post-listing.tsx
Fixing line formatting. (#947)
[lemmy-ui.git] / src / shared / components / post / post-listing.tsx
1 import classNames from "classnames";
2 import { Component, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
4 import {
5   AddAdmin,
6   AddModToCommunity,
7   BanFromCommunity,
8   BanPerson,
9   BlockPerson,
10   CommunityModeratorView,
11   CreatePostLike,
12   CreatePostReport,
13   DeletePost,
14   FeaturePost,
15   Language,
16   LockPost,
17   PersonViewSafe,
18   PostFeatureType,
19   PostView,
20   PurgePerson,
21   PurgePost,
22   RemovePost,
23   SavePost,
24   TransferCommunity,
25 } from "lemmy-js-client";
26 import { externalHost } from "../../env";
27 import { i18n } from "../../i18next";
28 import { BanType, PurgeType } from "../../interfaces";
29 import { UserService, WebSocketService } from "../../services";
30 import {
31   amAdmin,
32   amCommunityCreator,
33   canAdmin,
34   canMod,
35   futureDaysToUnixTime,
36   hostname,
37   isAdmin,
38   isBanned,
39   isImage,
40   isMod,
41   isVideo,
42   mdNoImages,
43   mdToHtml,
44   mdToHtmlInline,
45   myAuth,
46   numToSI,
47   relTags,
48   setupTippy,
49   showScores,
50   wsClient,
51 } from "../../utils";
52 import { Icon, PurgeWarning, Spinner } from "../common/icon";
53 import { MomentTime } from "../common/moment-time";
54 import { PictrsImage } from "../common/pictrs-image";
55 import { CommunityLink } from "../community/community-link";
56 import { PersonListing } from "../person/person-listing";
57 import { MetadataCard } from "./metadata-card";
58 import { PostForm } from "./post-form";
59
60 interface PostListingState {
61   showEdit: boolean;
62   showRemoveDialog: boolean;
63   showPurgeDialog: boolean;
64   purgeReason?: string;
65   purgeType?: PurgeType;
66   purgeLoading: boolean;
67   removeReason?: string;
68   showBanDialog: boolean;
69   banReason?: string;
70   banExpireDays?: number;
71   banType?: BanType;
72   removeData?: boolean;
73   showConfirmTransferSite: boolean;
74   showConfirmTransferCommunity: boolean;
75   imageExpanded: boolean;
76   viewSource: boolean;
77   showAdvanced: boolean;
78   showMoreMobile: boolean;
79   showBody: boolean;
80   showReportDialog: boolean;
81   reportReason?: string;
82   my_vote?: number;
83   score: number;
84   upvotes: number;
85   downvotes: number;
86 }
87
88 interface PostListingProps {
89   post_view: PostView;
90   duplicates?: PostView[];
91   moderators?: CommunityModeratorView[];
92   admins?: PersonViewSafe[];
93   allLanguages: Language[];
94   siteLanguages: number[];
95   showCommunity?: boolean;
96   showBody?: boolean;
97   hideImage?: boolean;
98   enableDownvotes?: boolean;
99   enableNsfw?: boolean;
100   viewOnly?: boolean;
101 }
102
103 export class PostListing extends Component<PostListingProps, PostListingState> {
104   state: PostListingState = {
105     showEdit: false,
106     showRemoveDialog: false,
107     showPurgeDialog: false,
108     purgeType: PurgeType.Person,
109     purgeLoading: false,
110     showBanDialog: false,
111     banType: BanType.Community,
112     removeData: false,
113     showConfirmTransferSite: false,
114     showConfirmTransferCommunity: false,
115     imageExpanded: false,
116     viewSource: false,
117     showAdvanced: false,
118     showMoreMobile: false,
119     showBody: false,
120     showReportDialog: false,
121     my_vote: this.props.post_view.my_vote,
122     score: this.props.post_view.counts.score,
123     upvotes: this.props.post_view.counts.upvotes,
124     downvotes: this.props.post_view.counts.downvotes,
125   };
126
127   constructor(props: any, context: any) {
128     super(props, context);
129
130     this.handlePostLike = this.handlePostLike.bind(this);
131     this.handlePostDisLike = this.handlePostDisLike.bind(this);
132     this.handleEditPost = this.handleEditPost.bind(this);
133     this.handleEditCancel = this.handleEditCancel.bind(this);
134   }
135
136   componentWillReceiveProps(nextProps: PostListingProps) {
137     this.setState({
138       my_vote: nextProps.post_view.my_vote,
139       upvotes: nextProps.post_view.counts.upvotes,
140       downvotes: nextProps.post_view.counts.downvotes,
141       score: nextProps.post_view.counts.score,
142     });
143     if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
144       this.setState({ imageExpanded: false });
145     }
146   }
147
148   render() {
149     let post = this.props.post_view.post;
150     return (
151       <div className="post-listing">
152         {!this.state.showEdit ? (
153           <>
154             {this.listing()}
155             {this.state.imageExpanded && !this.props.hideImage && this.img}
156             {post.url && this.showBody && post.embed_title && (
157               <MetadataCard post={post} />
158             )}
159             {this.showBody && this.body()}
160           </>
161         ) : (
162           <div className="col-12">
163             <PostForm
164               post_view={this.props.post_view}
165               onEdit={this.handleEditPost}
166               onCancel={this.handleEditCancel}
167               enableNsfw={this.props.enableNsfw}
168               enableDownvotes={this.props.enableDownvotes}
169               allLanguages={this.props.allLanguages}
170               siteLanguages={this.props.siteLanguages}
171             />
172           </div>
173         )}
174       </div>
175     );
176   }
177
178   body() {
179     let body = this.props.post_view.post.body;
180     return body ? (
181       <div className="col-12 card my-2 p-2">
182         {this.state.viewSource ? (
183           <pre>{body}</pre>
184         ) : (
185           <div className="md-div" dangerouslySetInnerHTML={mdToHtml(body)} />
186         )}
187       </div>
188     ) : (
189       <></>
190     );
191   }
192
193   get img() {
194     let src = this.imageSrc;
195     return src ? (
196       <>
197         <div className="offset-sm-3 my-2 d-none d-sm-block">
198           <a href={src} className="d-inline-block">
199             <PictrsImage src={src} />
200           </a>
201         </div>
202         <div className="my-2 d-block d-sm-none">
203           <a
204             className="d-inline-block"
205             onClick={linkEvent(this, this.handleImageExpandClick)}
206           >
207             <PictrsImage src={src} />
208           </a>
209         </div>
210       </>
211     ) : (
212       <></>
213     );
214   }
215
216   imgThumb(src: string) {
217     let post_view = this.props.post_view;
218     return (
219       <PictrsImage
220         src={src}
221         thumbnail
222         alt=""
223         nsfw={post_view.post.nsfw || post_view.community.nsfw}
224       />
225     );
226   }
227
228   get imageSrc(): string | undefined {
229     let post = this.props.post_view.post;
230     let url = post.url;
231     let thumbnail = post.thumbnail_url;
232
233     if (url && isImage(url)) {
234       if (url.includes("pictrs")) {
235         return url;
236       } else if (thumbnail) {
237         return thumbnail;
238       } else {
239         return url;
240       }
241     } else if (thumbnail) {
242       return thumbnail;
243     } else {
244       return undefined;
245     }
246   }
247
248   thumbnail() {
249     let post = this.props.post_view.post;
250     let url = post.url;
251     let thumbnail = post.thumbnail_url;
252
253     if (!this.props.hideImage && url && isImage(url) && this.imageSrc) {
254       return (
255         <a
256           href={this.imageSrc}
257           className="text-body d-inline-block position-relative mb-2"
258           data-tippy-content={i18n.t("expand_here")}
259           onClick={linkEvent(this, this.handleImageExpandClick)}
260           aria-label={i18n.t("expand_here")}
261         >
262           {this.imgThumb(this.imageSrc)}
263           <Icon icon="image" classes="mini-overlay" />
264         </a>
265       );
266     } else if (!this.props.hideImage && url && thumbnail && this.imageSrc) {
267       return (
268         <a
269           className="text-body d-inline-block position-relative mb-2"
270           href={url}
271           rel={relTags}
272           title={url}
273         >
274           {this.imgThumb(this.imageSrc)}
275           <Icon icon="external-link" classes="mini-overlay" />
276         </a>
277       );
278     } else if (url) {
279       if (!this.props.hideImage && isVideo(url)) {
280         return (
281           <div className="embed-responsive embed-responsive-16by9">
282             <video
283               playsInline
284               muted
285               loop
286               controls
287               className="embed-responsive-item"
288             >
289               <source src={url} type="video/mp4" />
290             </video>
291           </div>
292         );
293       } else {
294         return (
295           <a className="text-body" href={url} title={url} rel={relTags}>
296             <div className="thumbnail rounded bg-light d-flex justify-content-center">
297               <Icon icon="external-link" classes="d-flex align-items-center" />
298             </div>
299           </a>
300         );
301       }
302     } else {
303       return (
304         <Link
305           className="text-body"
306           to={`/post/${post.id}`}
307           title={i18n.t("comments")}
308         >
309           <div className="thumbnail rounded bg-light d-flex justify-content-center">
310             <Icon icon="message-square" classes="d-flex align-items-center" />
311           </div>
312         </Link>
313       );
314     }
315   }
316
317   createdLine() {
318     let post_view = this.props.post_view;
319     let url = post_view.post.url;
320     let body = post_view.post.body;
321     return (
322       <ul className="list-inline mb-1 text-muted small">
323         <li className="list-inline-item">
324           <PersonListing person={post_view.creator} />
325
326           {this.creatorIsMod_ && (
327             <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
328           )}
329           {this.creatorIsAdmin_ && (
330             <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
331           )}
332           {post_view.creator.bot_account && (
333             <span className="mx-1 badge badge-light">
334               {i18n.t("bot_account").toLowerCase()}
335             </span>
336           )}
337           {(post_view.creator_banned_from_community ||
338             isBanned(post_view.creator)) && (
339             <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
340           )}
341           {post_view.creator_blocked && (
342             <span className="mx-1 badge badge-danger">{"blocked"}</span>
343           )}
344           {this.props.showCommunity && (
345             <span>
346               <span className="mx-1"> {i18n.t("to")} </span>
347               <CommunityLink community={post_view.community} />
348             </span>
349           )}
350         </li>
351         <li className="list-inline-item">•</li>
352         {url && !(hostname(url) == externalHost) && (
353           <>
354             <li className="list-inline-item">
355               <a
356                 className="text-muted font-italic"
357                 href={url}
358                 title={url}
359                 rel={relTags}
360               >
361                 {hostname(url)}
362               </a>
363             </li>
364             <li className="list-inline-item">•</li>
365           </>
366         )}
367         <li className="list-inline-item">
368           <span>
369             <MomentTime
370               published={post_view.post.published}
371               updated={post_view.post.updated}
372             />
373           </span>
374         </li>
375         {body && (
376           <>
377             <li className="list-inline-item">•</li>
378             <li className="list-inline-item">
379               <button
380                 className="text-muted btn btn-sm btn-link p-0"
381                 data-tippy-content={mdNoImages.render(body)}
382                 data-tippy-allowHtml={true}
383                 onClick={linkEvent(this, this.handleShowBody)}
384               >
385                 <Icon icon="book-open" classes="icon-inline mr-1" />
386               </button>
387             </li>
388           </>
389         )}
390       </ul>
391     );
392   }
393
394   voteBar() {
395     return (
396       <div className={`vote-bar col-1 pr-0 small text-center`}>
397         <button
398           className={`btn-animate btn btn-link p-0 ${
399             this.state.my_vote == 1 ? "text-info" : "text-muted"
400           }`}
401           onClick={this.handlePostLike}
402           data-tippy-content={i18n.t("upvote")}
403           aria-label={i18n.t("upvote")}
404         >
405           <Icon icon="arrow-up1" classes="upvote" />
406         </button>
407         {showScores() ? (
408           <div
409             className={`unselectable pointer font-weight-bold text-muted px-1`}
410             data-tippy-content={this.pointsTippy}
411           >
412             {numToSI(this.state.score)}
413           </div>
414         ) : (
415           <div className="p-1"></div>
416         )}
417         {this.props.enableDownvotes && (
418           <button
419             className={`btn-animate btn btn-link p-0 ${
420               this.state.my_vote == -1 ? "text-danger" : "text-muted"
421             }`}
422             onClick={this.handlePostDisLike}
423             data-tippy-content={i18n.t("downvote")}
424             aria-label={i18n.t("downvote")}
425           >
426             <Icon icon="arrow-down1" classes="downvote" />
427           </button>
428         )}
429       </div>
430     );
431   }
432
433   get postLink() {
434     let post = this.props.post_view.post;
435     return (
436       <Link
437         className={`d-inline-block ${
438           !post.featured_community && !post.featured_local
439             ? "text-body"
440             : "text-primary"
441         }`}
442         to={`/post/${post.id}`}
443         title={i18n.t("comments")}
444       >
445         <div
446           className="d-inline-block"
447           dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
448         />
449       </Link>
450     );
451   }
452
453   postTitleLine() {
454     let post = this.props.post_view.post;
455     let url = post.url;
456
457     return (
458       <div className="post-title overflow-hidden">
459         <h5>
460           {url ? (
461             this.props.showBody ? (
462               <a
463                 className={`d-inline-block ${
464                   !post.featured_community && !post.featured_local
465                     ? "text-body"
466                     : "text-primary"
467                 }`}
468                 href={url}
469                 title={url}
470                 rel={relTags}
471               >
472                 <div
473                   className="d-inline-block"
474                   dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
475                 />
476               </a>
477             ) : (
478               this.postLink
479             )
480           ) : (
481             this.postLink
482           )}
483           {(url && isImage(url)) ||
484             (post.thumbnail_url && (
485               <button
486                 className="btn btn-link text-monospace text-muted small d-inline-block"
487                 data-tippy-content={i18n.t("expand_here")}
488                 onClick={linkEvent(this, this.handleImageExpandClick)}
489               >
490                 <Icon
491                   icon={
492                     !this.state.imageExpanded ? "plus-square" : "minus-square"
493                   }
494                   classes="icon-inline"
495                 />
496               </button>
497             ))}
498           {post.removed && (
499             <small className="ml-2 text-muted font-italic">
500               {i18n.t("removed")}
501             </small>
502           )}
503           {post.deleted && (
504             <small
505               className="unselectable pointer ml-2 text-muted font-italic"
506               data-tippy-content={i18n.t("deleted")}
507             >
508               <Icon icon="trash" classes="icon-inline text-danger" />
509             </small>
510           )}
511           {post.locked && (
512             <small
513               className="unselectable pointer ml-2 text-muted font-italic"
514               data-tippy-content={i18n.t("locked")}
515             >
516               <Icon icon="lock" classes="icon-inline text-danger" />
517             </small>
518           )}
519           {post.featured_community && (
520             <small
521               className="unselectable pointer ml-2 text-muted font-italic"
522               data-tippy-content={i18n.t("featured")}
523             >
524               <Icon icon="pin" classes="icon-inline text-primary" />
525             </small>
526           )}
527           {post.featured_local && (
528             <small
529               className="unselectable pointer ml-2 text-muted font-italic"
530               data-tippy-content={i18n.t("featured")}
531             >
532               <Icon icon="pin" classes="icon-inline text-secondary" />
533             </small>
534           )}
535           {post.nsfw && (
536             <small className="ml-2 text-muted font-italic">
537               {i18n.t("nsfw")}
538             </small>
539           )}
540         </h5>
541       </div>
542     );
543   }
544
545   duplicatesLine() {
546     let dupes = this.props.duplicates;
547     return dupes && dupes.length > 0 ? (
548       <ul className="list-inline mb-1 small text-muted">
549         <>
550           <li className="list-inline-item mr-2">{i18n.t("cross_posted_to")}</li>
551           {dupes.map(pv => (
552             <li key={pv.post.id} className="list-inline-item mr-2">
553               <Link to={`/post/${pv.post.id}`}>
554                 {pv.community.local
555                   ? pv.community.name
556                   : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
557               </Link>
558             </li>
559           ))}
560         </>
561       </ul>
562     ) : (
563       <></>
564     );
565   }
566
567   commentsLine(mobile = false) {
568     let post = this.props.post_view.post;
569     return (
570       <div className="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1">
571         {this.commentsButton}
572         {!post.local && (
573           <a
574             className="btn btn-link btn-animate text-muted py-0"
575             title={i18n.t("link")}
576             href={post.ap_id}
577           >
578             <Icon icon="fedilink" inline />
579           </a>
580         )}
581         {mobile && !this.props.viewOnly && this.mobileVotes}
582         {UserService.Instance.myUserInfo &&
583           !this.props.viewOnly &&
584           this.postActions(mobile)}
585       </div>
586     );
587   }
588
589   postActions(mobile = false) {
590     // Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
591     // Possible enhancement: Make each button a component.
592     let post_view = this.props.post_view;
593     return (
594       <>
595         {this.saveButton}
596         {this.crossPostButton}
597         {mobile && this.showMoreButton}
598         {(!mobile || this.state.showAdvanced) && (
599           <>
600             {!this.myPost && (
601               <>
602                 {this.reportButton}
603                 {this.blockButton}
604               </>
605             )}
606             {this.myPost && (this.showBody || this.state.showAdvanced) && (
607               <>
608                 {this.editButton}
609                 {this.deleteButton}
610               </>
611             )}
612           </>
613         )}
614         {this.state.showAdvanced && (
615           <>
616             {this.showBody && post_view.post.body && this.viewSourceButton}
617             {this.canModOnSelf_ && (
618               <>
619                 {this.lockButton}
620                 {this.featureButton}
621               </>
622             )}
623             {(this.canMod_ || this.canAdmin_) && <>{this.modRemoveButton}</>}
624           </>
625         )}
626         {!mobile && this.showMoreButton}
627       </>
628     );
629   }
630
631   get commentsButton() {
632     let post_view = this.props.post_view;
633     return (
634       <button className="btn btn-link text-muted py-0 pl-0">
635         <Link
636           className="text-muted"
637           title={i18n.t("number_of_comments", {
638             count: post_view.counts.comments,
639             formattedCount: post_view.counts.comments,
640           })}
641           to={`/post/${post_view.post.id}?scrollToComments=true`}
642         >
643           <Icon icon="message-square" classes="mr-1" inline />
644           <span className="mr-2">
645             {i18n.t("number_of_comments", {
646               count: post_view.counts.comments,
647               formattedCount: numToSI(post_view.counts.comments),
648             })}
649           </span>
650           {this.unreadCount && (
651             <span className="small text-warning">
652               ({this.unreadCount} {i18n.t("new")})
653             </span>
654           )}
655         </Link>
656       </button>
657     );
658   }
659
660   get unreadCount(): number | undefined {
661     let pv = this.props.post_view;
662     return pv.unread_comments == pv.counts.comments || pv.unread_comments == 0
663       ? undefined
664       : pv.unread_comments;
665   }
666
667   get mobileVotes() {
668     // TODO: make nicer
669     let tippy = showScores() ? { "data-tippy-content": this.pointsTippy } : {};
670     return (
671       <>
672         <div>
673           <button
674             className={`btn-animate btn py-0 px-1 ${
675               this.state.my_vote == 1 ? "text-info" : "text-muted"
676             }`}
677             {...tippy}
678             onClick={this.handlePostLike}
679             aria-label={i18n.t("upvote")}
680           >
681             <Icon icon="arrow-up1" classes="icon-inline small" />
682             {showScores() && (
683               <span className="ml-2">{numToSI(this.state.upvotes)}</span>
684             )}
685           </button>
686           {this.props.enableDownvotes && (
687             <button
688               className={`ml-2 btn-animate btn py-0 px-1 ${
689                 this.state.my_vote == -1 ? "text-danger" : "text-muted"
690               }`}
691               onClick={this.handlePostDisLike}
692               {...tippy}
693               aria-label={i18n.t("downvote")}
694             >
695               <Icon icon="arrow-down1" classes="icon-inline small" />
696               {showScores() && (
697                 <span
698                   className={classNames("ml-2", {
699                     invisible: this.state.downvotes === 0,
700                   })}
701                 >
702                   {numToSI(this.state.downvotes)}
703                 </span>
704               )}
705             </button>
706           )}
707         </div>
708       </>
709     );
710   }
711
712   get saveButton() {
713     let saved = this.props.post_view.saved;
714     let label = saved ? i18n.t("unsave") : i18n.t("save");
715     return (
716       <button
717         className="btn btn-link btn-animate text-muted py-0"
718         onClick={linkEvent(this, this.handleSavePostClick)}
719         data-tippy-content={label}
720         aria-label={label}
721       >
722         <Icon
723           icon="star"
724           classes={classNames({ "text-warning": saved })}
725           inline
726         />
727       </button>
728     );
729   }
730
731   get crossPostButton() {
732     return (
733       <Link
734         className="btn btn-link btn-animate text-muted py-0"
735         to={`/create_post${this.crossPostParams}`}
736         title={i18n.t("cross_post")}
737       >
738         <Icon icon="copy" inline />
739       </Link>
740     );
741   }
742
743   get reportButton() {
744     return (
745       <button
746         className="btn btn-link btn-animate text-muted py-0"
747         onClick={linkEvent(this, this.handleShowReportDialog)}
748         data-tippy-content={i18n.t("show_report_dialog")}
749         aria-label={i18n.t("show_report_dialog")}
750       >
751         <Icon icon="flag" inline />
752       </button>
753     );
754   }
755
756   get blockButton() {
757     return (
758       <button
759         className="btn btn-link btn-animate text-muted py-0"
760         onClick={linkEvent(this, this.handleBlockUserClick)}
761         data-tippy-content={i18n.t("block_user")}
762         aria-label={i18n.t("block_user")}
763       >
764         <Icon icon="slash" inline />
765       </button>
766     );
767   }
768
769   get editButton() {
770     return (
771       <button
772         className="btn btn-link btn-animate text-muted py-0"
773         onClick={linkEvent(this, this.handleEditClick)}
774         data-tippy-content={i18n.t("edit")}
775         aria-label={i18n.t("edit")}
776       >
777         <Icon icon="edit" inline />
778       </button>
779     );
780   }
781
782   get deleteButton() {
783     let deleted = this.props.post_view.post.deleted;
784     let label = !deleted ? i18n.t("delete") : i18n.t("restore");
785     return (
786       <button
787         className="btn btn-link btn-animate text-muted py-0"
788         onClick={linkEvent(this, this.handleDeleteClick)}
789         data-tippy-content={label}
790         aria-label={label}
791       >
792         <Icon
793           icon="trash"
794           classes={classNames({ "text-danger": deleted })}
795           inline
796         />
797       </button>
798     );
799   }
800
801   get showMoreButton() {
802     return (
803       <button
804         className="btn btn-link btn-animate text-muted py-0"
805         onClick={linkEvent(this, this.handleShowAdvanced)}
806         data-tippy-content={i18n.t("more")}
807         aria-label={i18n.t("more")}
808       >
809         <Icon icon="more-vertical" inline />
810       </button>
811     );
812   }
813
814   get viewSourceButton() {
815     return (
816       <button
817         className="btn btn-link btn-animate text-muted py-0"
818         onClick={linkEvent(this, this.handleViewSource)}
819         data-tippy-content={i18n.t("view_source")}
820         aria-label={i18n.t("view_source")}
821       >
822         <Icon
823           icon="file-text"
824           classes={classNames({ "text-success": this.state.viewSource })}
825           inline
826         />
827       </button>
828     );
829   }
830
831   get lockButton() {
832     let locked = this.props.post_view.post.locked;
833     let label = locked ? i18n.t("unlock") : i18n.t("lock");
834     return (
835       <button
836         className="btn btn-link btn-animate text-muted py-0"
837         onClick={linkEvent(this, this.handleModLock)}
838         data-tippy-content={label}
839         aria-label={label}
840       >
841         <Icon
842           icon="lock"
843           classes={classNames({ "text-danger": locked })}
844           inline
845         />
846       </button>
847     );
848   }
849
850   get featureButton() {
851     const featured_community = this.props.post_view.post.featured_community;
852     const label_community = featured_community
853       ? i18n.t("unfeature_from_community")
854       : i18n.t("feature_in_community");
855
856     const is_admin = amAdmin();
857     const featured_local = this.props.post_view.post.featured_local;
858     const label_local = featured_local
859       ? i18n.t("unfeature_from_local")
860       : i18n.t("feature_in_local");
861     return (
862       <span>
863         <button
864           className="btn btn-link btn-animate text-muted py-0 pl-0"
865           onClick={() => this.handleModFeaturePost(this, true)}
866           data-tippy-content={label_community}
867           aria-label={label_community}
868         >
869           <Icon
870             icon="pin"
871             classes={classNames({ "text-success": featured_community })}
872             inline
873           />{" "}
874           Community
875         </button>
876         {is_admin && (
877           <button
878             className="btn btn-link btn-animate text-muted py-0"
879             onClick={() => this.handleModFeaturePost(this, false)}
880             data-tippy-content={label_local}
881             aria-label={label_local}
882           >
883             <Icon
884               icon="pin"
885               classes={classNames({ "text-success": featured_local })}
886               inline
887             />{" "}
888             Local
889           </button>
890         )}
891       </span>
892     );
893   }
894
895   get modRemoveButton() {
896     let removed = this.props.post_view.post.removed;
897     return (
898       <button
899         className="btn btn-link btn-animate text-muted py-0"
900         onClick={linkEvent(
901           this,
902           !removed ? this.handleModRemoveShow : this.handleModRemoveSubmit
903         )}
904       >
905         {/* TODO: Find an icon for this. */}
906         {!removed ? i18n.t("remove") : i18n.t("restore")}
907       </button>
908     );
909   }
910
911   /**
912    * Mod/Admin actions to be taken against the author.
913    */
914   userActionsLine() {
915     // TODO: make nicer
916     let post_view = this.props.post_view;
917     return (
918       this.state.showAdvanced && (
919         <>
920           {this.canMod_ && (
921             <>
922               {!this.creatorIsMod_ &&
923                 (!post_view.creator_banned_from_community ? (
924                   <button
925                     className="btn btn-link btn-animate text-muted py-0"
926                     onClick={linkEvent(
927                       this,
928                       this.handleModBanFromCommunityShow
929                     )}
930                     aria-label={i18n.t("ban")}
931                   >
932                     {i18n.t("ban")}
933                   </button>
934                 ) : (
935                   <button
936                     className="btn btn-link btn-animate text-muted py-0"
937                     onClick={linkEvent(
938                       this,
939                       this.handleModBanFromCommunitySubmit
940                     )}
941                     aria-label={i18n.t("unban")}
942                   >
943                     {i18n.t("unban")}
944                   </button>
945                 ))}
946               {!post_view.creator_banned_from_community && (
947                 <button
948                   className="btn btn-link btn-animate text-muted py-0"
949                   onClick={linkEvent(this, this.handleAddModToCommunity)}
950                   aria-label={
951                     this.creatorIsMod_
952                       ? i18n.t("remove_as_mod")
953                       : i18n.t("appoint_as_mod")
954                   }
955                 >
956                   {this.creatorIsMod_
957                     ? i18n.t("remove_as_mod")
958                     : i18n.t("appoint_as_mod")}
959                 </button>
960               )}
961             </>
962           )}
963           {/* Community creators and admins can transfer community to another mod */}
964           {(amCommunityCreator(post_view.creator.id, this.props.moderators) ||
965             this.canAdmin_) &&
966             this.creatorIsMod_ &&
967             (!this.state.showConfirmTransferCommunity ? (
968               <button
969                 className="btn btn-link btn-animate text-muted py-0"
970                 onClick={linkEvent(
971                   this,
972                   this.handleShowConfirmTransferCommunity
973                 )}
974                 aria-label={i18n.t("transfer_community")}
975               >
976                 {i18n.t("transfer_community")}
977               </button>
978             ) : (
979               <>
980                 <button
981                   className="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
982                   aria-label={i18n.t("are_you_sure")}
983                 >
984                   {i18n.t("are_you_sure")}
985                 </button>
986                 <button
987                   className="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
988                   aria-label={i18n.t("yes")}
989                   onClick={linkEvent(this, this.handleTransferCommunity)}
990                 >
991                   {i18n.t("yes")}
992                 </button>
993                 <button
994                   className="btn btn-link btn-animate text-muted py-0 d-inline-block"
995                   onClick={linkEvent(
996                     this,
997                     this.handleCancelShowConfirmTransferCommunity
998                   )}
999                   aria-label={i18n.t("no")}
1000                 >
1001                   {i18n.t("no")}
1002                 </button>
1003               </>
1004             ))}
1005           {/* Admins can ban from all, and appoint other admins */}
1006           {this.canAdmin_ && (
1007             <>
1008               {!this.creatorIsAdmin_ && (
1009                 <>
1010                   {!isBanned(post_view.creator) ? (
1011                     <button
1012                       className="btn btn-link btn-animate text-muted py-0"
1013                       onClick={linkEvent(this, this.handleModBanShow)}
1014                       aria-label={i18n.t("ban_from_site")}
1015                     >
1016                       {i18n.t("ban_from_site")}
1017                     </button>
1018                   ) : (
1019                     <button
1020                       className="btn btn-link btn-animate text-muted py-0"
1021                       onClick={linkEvent(this, this.handleModBanSubmit)}
1022                       aria-label={i18n.t("unban_from_site")}
1023                     >
1024                       {i18n.t("unban_from_site")}
1025                     </button>
1026                   )}
1027                   <button
1028                     className="btn btn-link btn-animate text-muted py-0"
1029                     onClick={linkEvent(this, this.handlePurgePersonShow)}
1030                     aria-label={i18n.t("purge_user")}
1031                   >
1032                     {i18n.t("purge_user")}
1033                   </button>
1034                   <button
1035                     className="btn btn-link btn-animate text-muted py-0"
1036                     onClick={linkEvent(this, this.handlePurgePostShow)}
1037                     aria-label={i18n.t("purge_post")}
1038                   >
1039                     {i18n.t("purge_post")}
1040                   </button>
1041                 </>
1042               )}
1043               {!isBanned(post_view.creator) && post_view.creator.local && (
1044                 <button
1045                   className="btn btn-link btn-animate text-muted py-0"
1046                   onClick={linkEvent(this, this.handleAddAdmin)}
1047                   aria-label={
1048                     this.creatorIsAdmin_
1049                       ? i18n.t("remove_as_admin")
1050                       : i18n.t("appoint_as_admin")
1051                   }
1052                 >
1053                   {this.creatorIsAdmin_
1054                     ? i18n.t("remove_as_admin")
1055                     : i18n.t("appoint_as_admin")}
1056                 </button>
1057               )}
1058             </>
1059           )}
1060         </>
1061       )
1062     );
1063   }
1064
1065   removeAndBanDialogs() {
1066     let post = this.props.post_view;
1067     let purgeTypeText =
1068       this.state.purgeType == PurgeType.Post
1069         ? i18n.t("purge_post")
1070         : `${i18n.t("purge")} ${post.creator.name}`;
1071     return (
1072       <>
1073         {this.state.showRemoveDialog && (
1074           <form
1075             className="form-inline"
1076             onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
1077           >
1078             <label className="sr-only" htmlFor="post-listing-remove-reason">
1079               {i18n.t("reason")}
1080             </label>
1081             <input
1082               type="text"
1083               id="post-listing-remove-reason"
1084               className="form-control mr-2"
1085               placeholder={i18n.t("reason")}
1086               value={this.state.removeReason}
1087               onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1088             />
1089             <button
1090               type="submit"
1091               className="btn btn-secondary"
1092               aria-label={i18n.t("remove_post")}
1093             >
1094               {i18n.t("remove_post")}
1095             </button>
1096           </form>
1097         )}
1098         {this.state.showBanDialog && (
1099           <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1100             <div className="form-group row col-12">
1101               <label
1102                 className="col-form-label"
1103                 htmlFor="post-listing-ban-reason"
1104               >
1105                 {i18n.t("reason")}
1106               </label>
1107               <input
1108                 type="text"
1109                 id="post-listing-ban-reason"
1110                 className="form-control mr-2"
1111                 placeholder={i18n.t("reason")}
1112                 value={this.state.banReason}
1113                 onInput={linkEvent(this, this.handleModBanReasonChange)}
1114               />
1115               <label className="col-form-label" htmlFor={`mod-ban-expires`}>
1116                 {i18n.t("expires")}
1117               </label>
1118               <input
1119                 type="number"
1120                 id={`mod-ban-expires`}
1121                 className="form-control mr-2"
1122                 placeholder={i18n.t("number_of_days")}
1123                 value={this.state.banExpireDays}
1124                 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1125               />
1126               <div className="form-group">
1127                 <div className="form-check">
1128                   <input
1129                     className="form-check-input"
1130                     id="mod-ban-remove-data"
1131                     type="checkbox"
1132                     checked={this.state.removeData}
1133                     onChange={linkEvent(this, this.handleModRemoveDataChange)}
1134                   />
1135                   <label
1136                     className="form-check-label"
1137                     htmlFor="mod-ban-remove-data"
1138                     title={i18n.t("remove_content_more")}
1139                   >
1140                     {i18n.t("remove_content")}
1141                   </label>
1142                 </div>
1143               </div>
1144             </div>
1145             {/* TODO hold off on expires until later */}
1146             {/* <div class="form-group row"> */}
1147             {/*   <label class="col-form-label">Expires</label> */}
1148             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1149             {/* </div> */}
1150             <div className="form-group row">
1151               <button
1152                 type="submit"
1153                 className="btn btn-secondary"
1154                 aria-label={i18n.t("ban")}
1155               >
1156                 {i18n.t("ban")} {post.creator.name}
1157               </button>
1158             </div>
1159           </form>
1160         )}
1161         {this.state.showReportDialog && (
1162           <form
1163             className="form-inline"
1164             onSubmit={linkEvent(this, this.handleReportSubmit)}
1165           >
1166             <label className="sr-only" htmlFor="post-report-reason">
1167               {i18n.t("reason")}
1168             </label>
1169             <input
1170               type="text"
1171               id="post-report-reason"
1172               className="form-control mr-2"
1173               placeholder={i18n.t("reason")}
1174               required
1175               value={this.state.reportReason}
1176               onInput={linkEvent(this, this.handleReportReasonChange)}
1177             />
1178             <button
1179               type="submit"
1180               className="btn btn-secondary"
1181               aria-label={i18n.t("create_report")}
1182             >
1183               {i18n.t("create_report")}
1184             </button>
1185           </form>
1186         )}
1187         {this.state.showPurgeDialog && (
1188           <form
1189             className="form-inline"
1190             onSubmit={linkEvent(this, this.handlePurgeSubmit)}
1191           >
1192             <PurgeWarning />
1193             <label className="sr-only" htmlFor="purge-reason">
1194               {i18n.t("reason")}
1195             </label>
1196             <input
1197               type="text"
1198               id="purge-reason"
1199               className="form-control mr-2"
1200               placeholder={i18n.t("reason")}
1201               value={this.state.purgeReason}
1202               onInput={linkEvent(this, this.handlePurgeReasonChange)}
1203             />
1204             {this.state.purgeLoading ? (
1205               <Spinner />
1206             ) : (
1207               <button
1208                 type="submit"
1209                 className="btn btn-secondary"
1210                 aria-label={purgeTypeText}
1211               >
1212                 {purgeTypeText}
1213               </button>
1214             )}
1215           </form>
1216         )}
1217       </>
1218     );
1219   }
1220
1221   mobileThumbnail() {
1222     let post = this.props.post_view.post;
1223     return post.thumbnail_url || (post.url && isImage(post.url)) ? (
1224       <div className="row">
1225         <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1226           {this.postTitleLine()}
1227         </div>
1228         <div className="col-4">
1229           {/* Post body prev or thumbnail */}
1230           {!this.state.imageExpanded && this.thumbnail()}
1231         </div>
1232       </div>
1233     ) : (
1234       this.postTitleLine()
1235     );
1236   }
1237
1238   showMobilePreview() {
1239     let body = this.props.post_view.post.body;
1240     return !this.showBody && body ? (
1241       <div className="md-div mb-1 preview-lines">{body}</div>
1242     ) : (
1243       <></>
1244     );
1245   }
1246
1247   listing() {
1248     return (
1249       <>
1250         {/* The mobile view*/}
1251         <div className="d-block d-sm-none">
1252           <div className="row">
1253             <div className="col-12">
1254               {this.createdLine()}
1255
1256               {/* If it has a thumbnail, do a right aligned thumbnail */}
1257               {this.mobileThumbnail()}
1258
1259               {/* Show a preview of the post body */}
1260               {this.showMobilePreview()}
1261
1262               {this.commentsLine(true)}
1263               {this.userActionsLine()}
1264               {this.duplicatesLine()}
1265               {this.removeAndBanDialogs()}
1266             </div>
1267           </div>
1268         </div>
1269
1270         {/* The larger view*/}
1271         <div className="d-none d-sm-block">
1272           <div className="row">
1273             {!this.props.viewOnly && this.voteBar()}
1274             <div className="col-sm-2 pr-0">
1275               <div className="">{this.thumbnail()}</div>
1276             </div>
1277             <div className="col-12 col-sm-9">
1278               <div className="row">
1279                 <div className="col-12">
1280                   {this.postTitleLine()}
1281                   {this.createdLine()}
1282                   {this.commentsLine()}
1283                   {this.duplicatesLine()}
1284                   {this.userActionsLine()}
1285                   {this.removeAndBanDialogs()}
1286                 </div>
1287               </div>
1288             </div>
1289           </div>
1290         </div>
1291       </>
1292     );
1293   }
1294
1295   private get myPost(): boolean {
1296     return (
1297       this.props.post_view.creator.id ==
1298       UserService.Instance.myUserInfo?.local_user_view.person.id
1299     );
1300   }
1301
1302   handlePostLike(event: any) {
1303     event.preventDefault();
1304     if (!UserService.Instance.myUserInfo) {
1305       this.context.router.history.push(`/login`);
1306     }
1307
1308     let myVote = this.state.my_vote;
1309     let newVote = myVote == 1 ? 0 : 1;
1310
1311     if (myVote == 1) {
1312       this.setState({
1313         score: this.state.score - 1,
1314         upvotes: this.state.upvotes - 1,
1315       });
1316     } else if (myVote == -1) {
1317       this.setState({
1318         score: this.state.score + 2,
1319         upvotes: this.state.upvotes + 1,
1320         downvotes: this.state.downvotes - 1,
1321       });
1322     } else {
1323       this.setState({
1324         score: this.state.score + 1,
1325         upvotes: this.state.upvotes + 1,
1326       });
1327     }
1328
1329     this.setState({ my_vote: newVote });
1330
1331     let auth = myAuth();
1332     if (auth) {
1333       let form: CreatePostLike = {
1334         post_id: this.props.post_view.post.id,
1335         score: newVote,
1336         auth,
1337       };
1338
1339       WebSocketService.Instance.send(wsClient.likePost(form));
1340       this.setState(this.state);
1341     }
1342     setupTippy();
1343   }
1344
1345   handlePostDisLike(event: any) {
1346     event.preventDefault();
1347     if (!UserService.Instance.myUserInfo) {
1348       this.context.router.history.push(`/login`);
1349     }
1350
1351     let myVote = this.state.my_vote;
1352     let newVote = myVote == -1 ? 0 : -1;
1353
1354     if (myVote == 1) {
1355       this.setState({
1356         score: this.state.score - 2,
1357         upvotes: this.state.upvotes - 1,
1358         downvotes: this.state.downvotes + 1,
1359       });
1360     } else if (myVote == -1) {
1361       this.setState({
1362         score: this.state.score + 1,
1363         downvotes: this.state.downvotes - 1,
1364       });
1365     } else {
1366       this.setState({
1367         score: this.state.score - 1,
1368         downvotes: this.state.downvotes + 1,
1369       });
1370     }
1371
1372     this.setState({ my_vote: newVote });
1373
1374     let auth = myAuth();
1375     if (auth) {
1376       let form: CreatePostLike = {
1377         post_id: this.props.post_view.post.id,
1378         score: newVote,
1379         auth,
1380       };
1381
1382       WebSocketService.Instance.send(wsClient.likePost(form));
1383       this.setState(this.state);
1384     }
1385     setupTippy();
1386   }
1387
1388   handleEditClick(i: PostListing) {
1389     i.setState({ showEdit: true });
1390   }
1391
1392   handleEditCancel() {
1393     this.setState({ showEdit: false });
1394   }
1395
1396   // The actual editing is done in the recieve for post
1397   handleEditPost() {
1398     this.setState({ showEdit: false });
1399   }
1400
1401   handleShowReportDialog(i: PostListing) {
1402     i.setState({ showReportDialog: !i.state.showReportDialog });
1403   }
1404
1405   handleReportReasonChange(i: PostListing, event: any) {
1406     i.setState({ reportReason: event.target.value });
1407   }
1408
1409   handleReportSubmit(i: PostListing, event: any) {
1410     event.preventDefault();
1411     let auth = myAuth();
1412     let reason = i.state.reportReason;
1413     if (auth && reason) {
1414       let form: CreatePostReport = {
1415         post_id: i.props.post_view.post.id,
1416         reason,
1417         auth,
1418       };
1419       WebSocketService.Instance.send(wsClient.createPostReport(form));
1420
1421       i.setState({ showReportDialog: false });
1422     }
1423   }
1424
1425   handleBlockUserClick(i: PostListing) {
1426     let auth = myAuth();
1427     if (auth) {
1428       let blockUserForm: BlockPerson = {
1429         person_id: i.props.post_view.creator.id,
1430         block: true,
1431         auth,
1432       };
1433       WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1434     }
1435   }
1436
1437   handleDeleteClick(i: PostListing) {
1438     let auth = myAuth();
1439     if (auth) {
1440       let deleteForm: DeletePost = {
1441         post_id: i.props.post_view.post.id,
1442         deleted: !i.props.post_view.post.deleted,
1443         auth,
1444       };
1445       WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1446     }
1447   }
1448
1449   handleSavePostClick(i: PostListing) {
1450     let auth = myAuth();
1451     if (auth) {
1452       let saved =
1453         i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1454       let form: SavePost = {
1455         post_id: i.props.post_view.post.id,
1456         save: saved,
1457         auth,
1458       };
1459       WebSocketService.Instance.send(wsClient.savePost(form));
1460     }
1461   }
1462
1463   get crossPostParams(): string {
1464     let post = this.props.post_view.post;
1465     let params = `?title=${encodeURIComponent(post.name)}`;
1466
1467     if (post.url) {
1468       params += `&url=${encodeURIComponent(post.url)}`;
1469     }
1470     let crossPostBody = this.crossPostBody();
1471     if (crossPostBody) {
1472       params += `&body=${encodeURIComponent(crossPostBody)}`;
1473     }
1474     return params;
1475   }
1476
1477   crossPostBody(): string | undefined {
1478     let post = this.props.post_view.post;
1479     let body = post.body;
1480
1481     return body
1482       ? `${i18n.t("cross_posted_from")} ${post.ap_id}\n\n${body.replace(
1483           /^/gm,
1484           "> "
1485         )}`
1486       : undefined;
1487   }
1488
1489   get showBody(): boolean {
1490     return this.props.showBody || this.state.showBody;
1491   }
1492
1493   handleModRemoveShow(i: PostListing) {
1494     i.setState({
1495       showRemoveDialog: !i.state.showRemoveDialog,
1496       showBanDialog: false,
1497     });
1498   }
1499
1500   handleModRemoveReasonChange(i: PostListing, event: any) {
1501     i.setState({ removeReason: event.target.value });
1502   }
1503
1504   handleModRemoveDataChange(i: PostListing, event: any) {
1505     i.setState({ removeData: event.target.checked });
1506   }
1507
1508   handleModRemoveSubmit(i: PostListing, event: any) {
1509     event.preventDefault();
1510
1511     let auth = myAuth();
1512     if (auth) {
1513       let form: RemovePost = {
1514         post_id: i.props.post_view.post.id,
1515         removed: !i.props.post_view.post.removed,
1516         reason: i.state.removeReason,
1517         auth,
1518       };
1519       WebSocketService.Instance.send(wsClient.removePost(form));
1520       i.setState({ showRemoveDialog: false });
1521     }
1522   }
1523
1524   handleModLock(i: PostListing) {
1525     let auth = myAuth();
1526     if (auth) {
1527       let form: LockPost = {
1528         post_id: i.props.post_view.post.id,
1529         locked: !i.props.post_view.post.locked,
1530         auth,
1531       };
1532       WebSocketService.Instance.send(wsClient.lockPost(form));
1533     }
1534   }
1535
1536   handleModFeaturePost(i: PostListing, is_community: boolean) {
1537     let auth = myAuth();
1538     if (auth) {
1539       let featured: [PostFeatureType, boolean] = is_community
1540         ? [
1541             PostFeatureType.Community,
1542             !i.props.post_view.post.featured_community,
1543           ]
1544         : [PostFeatureType.Local, !i.props.post_view.post.featured_local];
1545
1546       let form: FeaturePost = {
1547         post_id: i.props.post_view.post.id,
1548         feature_type: featured[0],
1549         featured: featured[1],
1550         auth,
1551       };
1552       WebSocketService.Instance.send(wsClient.featurePost(form));
1553     }
1554   }
1555
1556   handleModBanFromCommunityShow(i: PostListing) {
1557     i.setState({
1558       showBanDialog: true,
1559       banType: BanType.Community,
1560       showRemoveDialog: false,
1561     });
1562   }
1563
1564   handleModBanShow(i: PostListing) {
1565     i.setState({
1566       showBanDialog: true,
1567       banType: BanType.Site,
1568       showRemoveDialog: false,
1569     });
1570   }
1571
1572   handlePurgePersonShow(i: PostListing) {
1573     i.setState({
1574       showPurgeDialog: true,
1575       purgeType: PurgeType.Person,
1576       showRemoveDialog: false,
1577     });
1578   }
1579
1580   handlePurgePostShow(i: PostListing) {
1581     i.setState({
1582       showPurgeDialog: true,
1583       purgeType: PurgeType.Post,
1584       showRemoveDialog: false,
1585     });
1586   }
1587
1588   handlePurgeReasonChange(i: PostListing, event: any) {
1589     i.setState({ purgeReason: event.target.value });
1590   }
1591
1592   handlePurgeSubmit(i: PostListing, event: any) {
1593     event.preventDefault();
1594
1595     let auth = myAuth();
1596     if (auth) {
1597       if (i.state.purgeType == PurgeType.Person) {
1598         let form: PurgePerson = {
1599           person_id: i.props.post_view.creator.id,
1600           reason: i.state.purgeReason,
1601           auth,
1602         };
1603         WebSocketService.Instance.send(wsClient.purgePerson(form));
1604       } else if (i.state.purgeType == PurgeType.Post) {
1605         let form: PurgePost = {
1606           post_id: i.props.post_view.post.id,
1607           reason: i.state.purgeReason,
1608           auth,
1609         };
1610         WebSocketService.Instance.send(wsClient.purgePost(form));
1611       }
1612
1613       i.setState({ purgeLoading: true });
1614     }
1615   }
1616
1617   handleModBanReasonChange(i: PostListing, event: any) {
1618     i.setState({ banReason: event.target.value });
1619   }
1620
1621   handleModBanExpireDaysChange(i: PostListing, event: any) {
1622     i.setState({ banExpireDays: event.target.value });
1623   }
1624
1625   handleModBanFromCommunitySubmit(i: PostListing) {
1626     i.setState({ banType: BanType.Community });
1627     i.handleModBanBothSubmit(i);
1628   }
1629
1630   handleModBanSubmit(i: PostListing) {
1631     i.setState({ banType: BanType.Site });
1632     i.handleModBanBothSubmit(i);
1633   }
1634
1635   handleModBanBothSubmit(i: PostListing, event?: any) {
1636     if (event) event.preventDefault();
1637     let auth = myAuth();
1638     if (auth) {
1639       let ban = !i.props.post_view.creator_banned_from_community;
1640       let person_id = i.props.post_view.creator.id;
1641       let remove_data = i.state.removeData;
1642       let reason = i.state.banReason;
1643       let expires = futureDaysToUnixTime(i.state.banExpireDays);
1644
1645       if (i.state.banType == BanType.Community) {
1646         // If its an unban, restore all their data
1647         if (ban == false) {
1648           i.setState({ removeData: false });
1649         }
1650
1651         let form: BanFromCommunity = {
1652           person_id,
1653           community_id: i.props.post_view.community.id,
1654           ban,
1655           remove_data,
1656           reason,
1657           expires,
1658           auth,
1659         };
1660         WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1661       } else {
1662         // If its an unban, restore all their data
1663         let ban = !i.props.post_view.creator.banned;
1664         if (ban == false) {
1665           i.setState({ removeData: false });
1666         }
1667         let form: BanPerson = {
1668           person_id,
1669           ban,
1670           remove_data,
1671           reason,
1672           expires,
1673           auth,
1674         };
1675         WebSocketService.Instance.send(wsClient.banPerson(form));
1676       }
1677
1678       i.setState({ showBanDialog: false });
1679     }
1680   }
1681
1682   handleAddModToCommunity(i: PostListing) {
1683     let auth = myAuth();
1684     if (auth) {
1685       let form: AddModToCommunity = {
1686         person_id: i.props.post_view.creator.id,
1687         community_id: i.props.post_view.community.id,
1688         added: !i.creatorIsMod_,
1689         auth,
1690       };
1691       WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1692       i.setState(i.state);
1693     }
1694   }
1695
1696   handleAddAdmin(i: PostListing) {
1697     let auth = myAuth();
1698     if (auth) {
1699       let form: AddAdmin = {
1700         person_id: i.props.post_view.creator.id,
1701         added: !i.creatorIsAdmin_,
1702         auth,
1703       };
1704       WebSocketService.Instance.send(wsClient.addAdmin(form));
1705       i.setState(i.state);
1706     }
1707   }
1708
1709   handleShowConfirmTransferCommunity(i: PostListing) {
1710     i.setState({ showConfirmTransferCommunity: true });
1711   }
1712
1713   handleCancelShowConfirmTransferCommunity(i: PostListing) {
1714     i.setState({ showConfirmTransferCommunity: false });
1715   }
1716
1717   handleTransferCommunity(i: PostListing) {
1718     let auth = myAuth();
1719     if (auth) {
1720       let form: TransferCommunity = {
1721         community_id: i.props.post_view.community.id,
1722         person_id: i.props.post_view.creator.id,
1723         auth,
1724       };
1725       WebSocketService.Instance.send(wsClient.transferCommunity(form));
1726       i.setState({ showConfirmTransferCommunity: false });
1727     }
1728   }
1729
1730   handleShowConfirmTransferSite(i: PostListing) {
1731     i.setState({ showConfirmTransferSite: true });
1732   }
1733
1734   handleCancelShowConfirmTransferSite(i: PostListing) {
1735     i.setState({ showConfirmTransferSite: false });
1736   }
1737
1738   handleImageExpandClick(i: PostListing, event: any) {
1739     event.preventDefault();
1740     i.setState({ imageExpanded: !i.state.imageExpanded });
1741     setupTippy();
1742   }
1743
1744   handleViewSource(i: PostListing) {
1745     i.setState({ viewSource: !i.state.viewSource });
1746   }
1747
1748   handleShowAdvanced(i: PostListing) {
1749     i.setState({ showAdvanced: !i.state.showAdvanced });
1750     setupTippy();
1751   }
1752
1753   handleShowMoreMobile(i: PostListing) {
1754     i.setState({
1755       showMoreMobile: !i.state.showMoreMobile,
1756       showAdvanced: !i.state.showAdvanced,
1757     });
1758     setupTippy();
1759   }
1760
1761   handleShowBody(i: PostListing) {
1762     i.setState({ showBody: !i.state.showBody });
1763     setupTippy();
1764   }
1765
1766   get pointsTippy(): string {
1767     let points = i18n.t("number_of_points", {
1768       count: this.state.score,
1769       formattedCount: this.state.score,
1770     });
1771
1772     let upvotes = i18n.t("number_of_upvotes", {
1773       count: this.state.upvotes,
1774       formattedCount: this.state.upvotes,
1775     });
1776
1777     let downvotes = i18n.t("number_of_downvotes", {
1778       count: this.state.downvotes,
1779       formattedCount: this.state.downvotes,
1780     });
1781
1782     return `${points} • ${upvotes} • ${downvotes}`;
1783   }
1784
1785   get canModOnSelf_(): boolean {
1786     return canMod(
1787       this.props.post_view.creator.id,
1788       this.props.moderators,
1789       this.props.admins,
1790       undefined,
1791       true
1792     );
1793   }
1794
1795   get canMod_(): boolean {
1796     return canMod(
1797       this.props.post_view.creator.id,
1798       this.props.moderators,
1799       this.props.admins
1800     );
1801   }
1802
1803   get canAdmin_(): boolean {
1804     return canAdmin(this.props.post_view.creator.id, this.props.admins);
1805   }
1806
1807   get creatorIsMod_(): boolean {
1808     return isMod(this.props.post_view.creator.id, this.props.moderators);
1809   }
1810
1811   get creatorIsAdmin_(): boolean {
1812     return isAdmin(this.props.post_view.creator.id, this.props.admins);
1813   }
1814 }