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