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