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