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