1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
8 CommunityModeratorView,
19 } from "lemmy-js-client";
20 import { externalHost } from "../../env";
21 import { i18n } from "../../i18next";
22 import { BanType } from "../../interfaces";
23 import { UserService, WebSocketService } from "../../services";
39 import { Icon } from "../common/icon";
40 import { MomentTime } from "../common/moment-time";
41 import { PictrsImage } from "../common/pictrs-image";
42 import { CommunityLink } from "../community/community-link";
43 import { PersonListing } from "../person/person-listing";
44 import { IFramelyCard } from "./iframely-card";
45 import { PostForm } from "./post-form";
47 interface PostListingState {
49 showRemoveDialog: boolean;
51 showBanDialog: boolean;
56 showConfirmTransferSite: boolean;
57 showConfirmTransferCommunity: boolean;
58 imageExpanded: boolean;
60 showAdvanced: boolean;
61 showMoreMobile: boolean;
69 interface PostListingProps {
71 duplicates?: PostView[];
72 showCommunity?: boolean;
74 moderators?: CommunityModeratorView[];
75 admins?: PersonViewSafe[];
76 enableDownvotes: boolean;
80 export class PostListing extends Component<PostListingProps, PostListingState> {
81 private emptyState: PostListingState = {
83 showRemoveDialog: false,
89 banType: BanType.Community,
90 showConfirmTransferSite: false,
91 showConfirmTransferCommunity: false,
95 showMoreMobile: false,
97 my_vote: this.props.post_view.my_vote,
98 score: this.props.post_view.counts.score,
99 upvotes: this.props.post_view.counts.upvotes,
100 downvotes: this.props.post_view.counts.downvotes,
103 constructor(props: any, context: any) {
104 super(props, context);
106 this.state = this.emptyState;
107 this.handlePostLike = this.handlePostLike.bind(this);
108 this.handlePostDisLike = this.handlePostDisLike.bind(this);
109 this.handleEditPost = this.handleEditPost.bind(this);
110 this.handleEditCancel = this.handleEditCancel.bind(this);
113 componentWillReceiveProps(nextProps: PostListingProps) {
114 this.state.my_vote = nextProps.post_view.my_vote;
115 this.state.upvotes = nextProps.post_view.counts.upvotes;
116 this.state.downvotes = nextProps.post_view.counts.downvotes;
117 this.state.score = nextProps.post_view.counts.score;
118 if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
119 this.state.imageExpanded = false;
121 this.setState(this.state);
127 {!this.state.showEdit ? (
135 post_view={this.props.post_view}
136 onEdit={this.handleEditPost}
137 onCancel={this.handleEditCancel}
138 enableNsfw={this.props.enableNsfw}
139 enableDownvotes={this.props.enableDownvotes}
148 let post = this.props.post_view.post;
152 {post.url && this.showBody && post.embed_title && (
153 <IFramelyCard post={post} />
157 (this.state.viewSource ? (
158 <pre>{post.body}</pre>
162 dangerouslySetInnerHTML={mdToHtml(post.body)}
170 imgThumb(src: string) {
171 let post_view = this.props.post_view;
177 nsfw={post_view.post.nsfw || post_view.community.nsfw}
182 getImageSrc(): string {
183 let post = this.props.post_view.post;
184 if (isImage(post.url)) {
185 if (post.url.includes("pictrs")) {
187 } else if (post.thumbnail_url) {
188 return post.thumbnail_url;
192 } else if (post.thumbnail_url) {
193 return post.thumbnail_url;
200 let post = this.props.post_view.post;
202 if (isImage(post.url)) {
205 class="float-right text-body pointer d-inline-block position-relative mb-2"
206 data-tippy-content={i18n.t("expand_here")}
207 onClick={linkEvent(this, this.handleImageExpandClick)}
209 aria-label={i18n.t("expand_here")}
211 {this.imgThumb(this.getImageSrc())}
212 <Icon icon="image" classes="mini-overlay" />
215 } else if (post.thumbnail_url) {
218 class="float-right text-body d-inline-block position-relative mb-2"
223 {this.imgThumb(this.getImageSrc())}
224 <Icon icon="external-link" classes="mini-overlay" />
227 } else if (post.url) {
228 if (isVideo(post.url)) {
230 <div class="embed-responsive embed-responsive-16by9">
236 class="embed-responsive-item"
238 <source src={post.url} type="video/mp4" />
245 className="text-body"
250 <div class="thumbnail rounded bg-light d-flex justify-content-center">
251 <Icon icon="external-link" classes="d-flex align-items-center" />
259 className="text-body"
260 to={`/post/${post.id}`}
261 title={i18n.t("comments")}
263 <div class="thumbnail rounded bg-light d-flex justify-content-center">
264 <Icon icon="message-square" classes="d-flex align-items-center" />
272 let post_view = this.props.post_view;
274 <ul class="list-inline mb-1 text-muted small">
275 <li className="list-inline-item">
276 <PersonListing person={post_view.creator} />
279 <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
282 <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
284 {(post_view.creator_banned_from_community ||
285 post_view.creator.banned) && (
286 <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
288 {this.props.showCommunity && (
290 <span class="mx-1"> {i18n.t("to")} </span>
291 <CommunityLink community={post_view.community} />
295 <li className="list-inline-item">•</li>
296 {post_view.post.url && !(hostname(post_view.post.url) == externalHost) && (
298 <li className="list-inline-item">
300 className="text-muted font-italic"
301 href={post_view.post.url}
302 title={post_view.post.url}
305 {hostname(post_view.post.url)}
308 <li className="list-inline-item">•</li>
311 <li className="list-inline-item">
313 <MomentTime data={post_view.post} />
316 {post_view.post.body && (
318 <li className="list-inline-item">•</li>
319 <li className="list-inline-item">
321 className="text-muted btn btn-sm btn-link p-0"
322 data-tippy-content={md.render(
323 previewLines(post_view.post.body)
325 data-tippy-allowHtml={true}
326 onClick={linkEvent(this, this.handleShowBody)}
328 <Icon icon="book-open" classes="icon-inline mr-1" />
339 <div className={`vote-bar col-1 pr-0 small text-center`}>
341 className={`btn-animate btn btn-link p-0 ${
342 this.state.my_vote == 1 ? "text-info" : "text-muted"
344 onClick={linkEvent(this, this.handlePostLike)}
345 data-tippy-content={i18n.t("upvote")}
346 aria-label={i18n.t("upvote")}
348 <Icon icon="arrow-up1" classes="upvote" />
352 class={`unselectable pointer font-weight-bold text-muted px-1`}
353 data-tippy-content={this.pointsTippy}
358 <div class="p-1"></div>
360 {this.props.enableDownvotes && (
362 className={`btn-animate btn btn-link p-0 ${
363 this.state.my_vote == -1 ? "text-danger" : "text-muted"
365 onClick={linkEvent(this, this.handlePostDisLike)}
366 data-tippy-content={i18n.t("downvote")}
367 aria-label={i18n.t("downvote")}
369 <Icon icon="arrow-down1" classes="downvote" />
377 let post = this.props.post_view.post;
379 <div className="post-title overflow-hidden">
381 {this.showBody && post.url ? (
383 className={!post.stickied ? "text-body" : "text-primary"}
392 className={!post.stickied ? "text-body" : "text-primary"}
393 to={`/post/${post.id}`}
394 title={i18n.t("comments")}
399 {(isImage(post.url) || post.thumbnail_url) &&
400 (!this.state.imageExpanded ? (
402 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
403 data-tippy-content={i18n.t("expand_here")}
404 onClick={linkEvent(this, this.handleImageExpandClick)}
406 <Icon icon="plus-square" classes="icon-inline" />
411 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
412 onClick={linkEvent(this, this.handleImageExpandClick)}
414 <Icon icon="minus-square" classes="icon-inline" />
418 class="btn btn-link d-inline-block"
419 onClick={linkEvent(this, this.handleImageExpandClick)}
421 <PictrsImage src={this.getImageSrc()} />
427 <small className="ml-2 text-muted font-italic">
433 className="unselectable pointer ml-2 text-muted font-italic"
434 data-tippy-content={i18n.t("deleted")}
436 <Icon icon="trash" classes="icon-inline text-danger" />
441 className="unselectable pointer ml-2 text-muted font-italic"
442 data-tippy-content={i18n.t("locked")}
444 <Icon icon="lock" classes="icon-inline text-danger" />
449 className="unselectable pointer ml-2 text-muted font-italic"
450 data-tippy-content={i18n.t("stickied")}
452 <Icon icon="pin" classes="icon-inline text-primary" />
456 <small className="ml-2 text-muted font-italic">
465 commentsLine(mobile = false) {
466 let post_view = this.props.post_view;
468 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
469 <button class="btn btn-link text-muted p-0">
471 className="text-muted small"
472 title={i18n.t("number_of_comments", {
473 count: post_view.counts.comments,
475 to={`/post/${post_view.post.id}?scrollToComments=true`}
477 <Icon icon="message-square" classes="icon-inline mr-1" />
478 {i18n.t("number_of_comments", {
479 count: post_view.counts.comments,
485 {this.state.downvotes !== 0 && showScores() && (
487 class="btn text-muted py-0 pr-0"
488 data-tippy-content={this.pointsTippy}
489 aria-label={i18n.t("downvote")}
492 <Icon icon="arrow-down1" classes="icon-inline mr-1" />
493 <span>{this.state.downvotes}</span>
499 class="btn btn-link btn-animate text-muted py-0"
500 onClick={linkEvent(this, this.handleSavePostClick)}
502 post_view.saved ? i18n.t("unsave") : i18n.t("save")
504 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
509 classes={`icon-inline ${post_view.saved && "text-warning"}`}
516 {/* This is an expanding spacer for mobile */}
517 <div className="flex-grow-1"></div>
523 className={`btn-animate btn py-0 px-1 ${
524 this.state.my_vote == 1 ? "text-info" : "text-muted"
526 data-tippy-content={this.pointsTippy}
527 onClick={linkEvent(this, this.handlePostLike)}
528 aria-label={i18n.t("upvote")}
530 <Icon icon="arrow-up1" classes="icon-inline small mr-2" />
535 className={`btn-animate btn py-0 px-1 ${
536 this.state.my_vote == 1 ? "text-info" : "text-muted"
538 onClick={linkEvent(this, this.handlePostLike)}
539 aria-label={i18n.t("upvote")}
541 <Icon icon="arrow-up1" classes="icon-inline small" />
544 {this.props.enableDownvotes &&
547 className={`ml-2 btn-animate btn py-0 pl-1 ${
548 this.state.my_vote == -1 ? "text-danger" : "text-muted"
550 onClick={linkEvent(this, this.handlePostDisLike)}
551 data-tippy-content={this.pointsTippy}
552 aria-label={i18n.t("downvote")}
554 <Icon icon="arrow-down1" classes="icon-inline small mr-2" />
555 {this.state.downvotes !== 0 && (
556 <span>{this.state.downvotes}</span>
561 className={`ml-2 btn-animate btn py-0 pl-1 ${
562 this.state.my_vote == -1 ? "text-danger" : "text-muted"
564 onClick={linkEvent(this, this.handlePostDisLike)}
565 aria-label={i18n.t("downvote")}
567 <Icon icon="arrow-down1" classes="icon-inline small" />
572 class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
573 onClick={linkEvent(this, this.handleSavePostClick)}
574 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
576 post_view.saved ? i18n.t("unsave") : i18n.t("save")
581 classes={`icon-inline ${post_view.saved && "text-warning"}`}
585 {!this.state.showMoreMobile && this.showBody && (
587 class="btn btn-link btn-animate text-muted py-0"
588 onClick={linkEvent(this, this.handleShowMoreMobile)}
589 aria-label={i18n.t("more")}
590 data-tippy-content={i18n.t("more")}
592 <Icon icon="more-vertical" classes="icon-inline" />
595 {this.state.showMoreMobile && this.postActions(mobile)}
603 let dupes = this.props.duplicates;
606 dupes.length > 0 && (
607 <ul class="list-inline mb-1 small text-muted">
609 <li className="list-inline-item mr-2">
610 {i18n.t("cross_posted_to")}
613 <li className="list-inline-item mr-2">
614 <Link to={`/post/${pv.post.id}`}>
617 : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
627 postActions(mobile = false) {
628 let post_view = this.props.post_view;
630 UserService.Instance.localUserView && (
636 class="btn btn-link btn-animate text-muted py-0 pl-0"
637 onClick={linkEvent(this, this.handleSavePostClick)}
639 post_view.saved ? i18n.t("unsave") : i18n.t("save")
642 post_view.saved ? i18n.t("unsave") : i18n.t("save")
647 classes={`icon-inline ${post_view.saved && "text-warning"}`}
652 className="btn btn-link btn-animate text-muted py-0"
653 to={`/create_post${this.crossPostParams}`}
654 title={i18n.t("cross_post")}
656 <Icon icon="copy" classes="icon-inline" />
660 {this.myPost && this.showBody && (
663 class="btn btn-link btn-animate text-muted py-0"
664 onClick={linkEvent(this, this.handleEditClick)}
665 data-tippy-content={i18n.t("edit")}
666 aria-label={i18n.t("edit")}
668 <Icon icon="edit" classes="icon-inline" />
671 class="btn btn-link btn-animate text-muted py-0"
672 onClick={linkEvent(this, this.handleDeleteClick)}
674 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
677 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
682 classes={`icon-inline ${
683 post_view.post.deleted && "text-danger"
690 {!this.state.showAdvanced && this.showBody ? (
692 class="btn btn-link btn-animate text-muted py-0"
693 onClick={linkEvent(this, this.handleShowAdvanced)}
694 data-tippy-content={i18n.t("more")}
695 aria-label={i18n.t("more")}
697 <Icon icon="more-vertical" classes="icon-inline" />
701 {this.showBody && post_view.post.body && (
703 class="btn btn-link btn-animate text-muted py-0"
704 onClick={linkEvent(this, this.handleViewSource)}
705 data-tippy-content={i18n.t("view_source")}
706 aria-label={i18n.t("view_source")}
710 classes={`icon-inline ${
711 this.state.viewSource && "text-success"
716 {this.canModOnSelf && (
719 class="btn btn-link btn-animate text-muted py-0"
720 onClick={linkEvent(this, this.handleModLock)}
722 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
725 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
730 classes={`icon-inline ${
731 post_view.post.locked && "text-danger"
736 class="btn btn-link btn-animate text-muted py-0"
737 onClick={linkEvent(this, this.handleModSticky)}
739 post_view.post.stickied
744 post_view.post.stickied
751 classes={`icon-inline ${
752 post_view.post.stickied && "text-success"
758 {/* Mods can ban from community, and appoint as mods to community */}
759 {(this.canMod || this.canAdmin) &&
760 (!post_view.post.removed ? (
762 class="btn btn-link btn-animate text-muted py-0"
763 onClick={linkEvent(this, this.handleModRemoveShow)}
764 aria-label={i18n.t("remove")}
770 class="btn btn-link btn-animate text-muted py-0"
771 onClick={linkEvent(this, this.handleModRemoveSubmit)}
772 aria-label={i18n.t("restore")}
780 (!post_view.creator_banned_from_community ? (
782 class="btn btn-link btn-animate text-muted py-0"
785 this.handleModBanFromCommunityShow
787 aria-label={i18n.t("ban")}
793 class="btn btn-link btn-animate text-muted py-0"
796 this.handleModBanFromCommunitySubmit
798 aria-label={i18n.t("unban")}
803 {!post_view.creator_banned_from_community && (
805 class="btn btn-link btn-animate text-muted py-0"
806 onClick={linkEvent(this, this.handleAddModToCommunity)}
809 ? i18n.t("remove_as_mod")
810 : i18n.t("appoint_as_mod")
814 ? i18n.t("remove_as_mod")
815 : i18n.t("appoint_as_mod")}
820 {/* Community creators and admins can transfer community to another mod */}
821 {(this.amCommunityCreator || this.canAdmin) &&
823 (!this.state.showConfirmTransferCommunity ? (
825 class="btn btn-link btn-animate text-muted py-0"
828 this.handleShowConfirmTransferCommunity
830 aria-label={i18n.t("transfer_community")}
832 {i18n.t("transfer_community")}
837 class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
838 aria-label={i18n.t("are_you_sure")}
840 {i18n.t("are_you_sure")}
843 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
844 aria-label={i18n.t("yes")}
845 onClick={linkEvent(this, this.handleTransferCommunity)}
850 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
853 this.handleCancelShowConfirmTransferCommunity
855 aria-label={i18n.t("no")}
861 {/* Admins can ban from all, and appoint other admins */}
865 (!post_view.creator.banned ? (
867 class="btn btn-link btn-animate text-muted py-0"
868 onClick={linkEvent(this, this.handleModBanShow)}
869 aria-label={i18n.t("ban_from_site")}
871 {i18n.t("ban_from_site")}
875 class="btn btn-link btn-animate text-muted py-0"
876 onClick={linkEvent(this, this.handleModBanSubmit)}
877 aria-label={i18n.t("unban_from_site")}
879 {i18n.t("unban_from_site")}
882 {!post_view.creator.banned && post_view.creator.local && (
884 class="btn btn-link btn-animate text-muted py-0"
885 onClick={linkEvent(this, this.handleAddAdmin)}
888 ? i18n.t("remove_as_admin")
889 : i18n.t("appoint_as_admin")
893 ? i18n.t("remove_as_admin")
894 : i18n.t("appoint_as_admin")}
899 {/* Site Creator can transfer to another admin */}
900 {this.amSiteCreator &&
902 (!this.state.showConfirmTransferSite ? (
904 class="btn btn-link btn-animate text-muted py-0"
907 this.handleShowConfirmTransferSite
909 aria-label={i18n.t("transfer_site")}
911 {i18n.t("transfer_site")}
916 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
917 aria-label={i18n.t("are_you_sure")}
919 {i18n.t("are_you_sure")}
922 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
923 onClick={linkEvent(this, this.handleTransferSite)}
924 aria-label={i18n.t("yes")}
929 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
932 this.handleCancelShowConfirmTransferSite
934 aria-label={i18n.t("no")}
947 removeAndBanDialogs() {
948 let post = this.props.post_view;
951 {this.state.showRemoveDialog && (
954 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
956 <label class="sr-only" htmlFor="post-listing-remove-reason">
961 id="post-listing-remove-reason"
962 class="form-control mr-2"
963 placeholder={i18n.t("reason")}
964 value={this.state.removeReason}
965 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
969 class="btn btn-secondary"
970 aria-label={i18n.t("remove_post")}
972 {i18n.t("remove_post")}
976 {this.state.showBanDialog && (
977 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
978 <div class="form-group row">
979 <label class="col-form-label" htmlFor="post-listing-ban-reason">
984 id="post-listing-ban-reason"
985 class="form-control mr-2"
986 placeholder={i18n.t("reason")}
987 value={this.state.banReason}
988 onInput={linkEvent(this, this.handleModBanReasonChange)}
990 <div class="form-group">
991 <div class="form-check">
993 class="form-check-input"
994 id="mod-ban-remove-data"
996 checked={this.state.removeData}
997 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1000 class="form-check-label"
1001 htmlFor="mod-ban-remove-data"
1002 title={i18n.t("remove_content_more")}
1004 {i18n.t("remove_content")}
1009 {/* TODO hold off on expires until later */}
1010 {/* <div class="form-group row"> */}
1011 {/* <label class="col-form-label">Expires</label> */}
1012 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1014 <div class="form-group row">
1017 class="btn btn-secondary"
1018 aria-label={i18n.t("ban")}
1020 {i18n.t("ban")} {post.creator.name}
1030 let post = this.props.post_view.post;
1031 return post.thumbnail_url || isImage(post.url) ? (
1033 <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1034 {this.postTitleLine()}
1037 {/* Post body prev or thumbnail */}
1038 {!this.state.imageExpanded && this.thumbnail()}
1042 this.postTitleLine()
1046 showMobilePreview() {
1047 let post = this.props.post_view.post;
1052 className="md-div mb-1"
1053 dangerouslySetInnerHTML={{
1054 __html: md.render(previewLines(post.body)),
1064 {/* The mobile view*/}
1065 <div class="d-block d-sm-none">
1067 <div class="col-12">
1068 {this.createdLine()}
1070 {/* If it has a thumbnail, do a right aligned thumbnail */}
1071 {this.mobileThumbnail()}
1073 {/* Show a preview of the post body */}
1074 {this.showMobilePreview()}
1076 {this.commentsLine(true)}
1077 {this.duplicatesLine()}
1078 {this.removeAndBanDialogs()}
1083 {/* The larger view*/}
1084 <div class="d-none d-sm-block">
1087 {!this.state.imageExpanded && (
1088 <div class="col-sm-2 pr-0">
1089 <div class="">{this.thumbnail()}</div>
1094 this.state.imageExpanded ? "col-12" : "col-12 col-sm-9"
1098 <div className="col-12">
1099 {this.postTitleLine()}
1100 {this.createdLine()}
1101 {this.commentsLine()}
1102 {this.duplicatesLine()}
1103 {this.postActions()}
1104 {this.removeAndBanDialogs()}
1114 private get myPost(): boolean {
1116 UserService.Instance.localUserView &&
1117 this.props.post_view.creator.id ==
1118 UserService.Instance.localUserView.person.id
1122 get isMod(): boolean {
1124 this.props.moderators &&
1126 this.props.moderators.map(m => m.moderator.id),
1127 this.props.post_view.creator.id
1132 get isAdmin(): boolean {
1134 this.props.admins &&
1136 this.props.admins.map(a => a.person.id),
1137 this.props.post_view.creator.id
1142 get canMod(): boolean {
1143 if (this.props.admins && this.props.moderators) {
1144 let adminsThenMods = this.props.admins
1145 .map(a => a.person.id)
1146 .concat(this.props.moderators.map(m => m.moderator.id));
1149 UserService.Instance.localUserView,
1151 this.props.post_view.creator.id
1158 get canModOnSelf(): boolean {
1159 if (this.props.admins && this.props.moderators) {
1160 let adminsThenMods = this.props.admins
1161 .map(a => a.person.id)
1162 .concat(this.props.moderators.map(m => m.moderator.id));
1165 UserService.Instance.localUserView,
1167 this.props.post_view.creator.id,
1175 get canAdmin(): boolean {
1177 this.props.admins &&
1179 UserService.Instance.localUserView,
1180 this.props.admins.map(a => a.person.id),
1181 this.props.post_view.creator.id
1186 get amCommunityCreator(): boolean {
1188 this.props.moderators &&
1189 UserService.Instance.localUserView &&
1190 this.props.post_view.creator.id !=
1191 UserService.Instance.localUserView.person.id &&
1192 UserService.Instance.localUserView.person.id ==
1193 this.props.moderators[0].moderator.id
1197 get amSiteCreator(): boolean {
1199 this.props.admins &&
1200 UserService.Instance.localUserView &&
1201 this.props.post_view.creator.id !=
1202 UserService.Instance.localUserView.person.id &&
1203 UserService.Instance.localUserView.person.id ==
1204 this.props.admins[0].person.id
1208 handlePostLike(i: PostListing, event: any) {
1209 event.preventDefault();
1210 if (!UserService.Instance.localUserView) {
1211 this.context.router.history.push(`/login`);
1214 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1216 if (i.state.my_vote == 1) {
1219 } else if (i.state.my_vote == -1) {
1220 i.state.downvotes--;
1228 i.state.my_vote = new_vote;
1230 let form: CreatePostLike = {
1231 post_id: i.props.post_view.post.id,
1232 score: i.state.my_vote,
1236 WebSocketService.Instance.send(wsClient.likePost(form));
1237 i.setState(i.state);
1241 handlePostDisLike(i: PostListing, event: any) {
1242 event.preventDefault();
1243 if (!UserService.Instance.localUserView) {
1244 this.context.router.history.push(`/login`);
1247 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1249 if (i.state.my_vote == 1) {
1252 i.state.downvotes++;
1253 } else if (i.state.my_vote == -1) {
1254 i.state.downvotes--;
1257 i.state.downvotes++;
1261 i.state.my_vote = new_vote;
1263 let form: CreatePostLike = {
1264 post_id: i.props.post_view.post.id,
1265 score: i.state.my_vote,
1269 WebSocketService.Instance.send(wsClient.likePost(form));
1270 i.setState(i.state);
1274 handleEditClick(i: PostListing) {
1275 i.state.showEdit = true;
1276 i.setState(i.state);
1279 handleEditCancel() {
1280 this.state.showEdit = false;
1281 this.setState(this.state);
1284 // The actual editing is done in the recieve for post
1286 this.state.showEdit = false;
1287 this.setState(this.state);
1290 handleDeleteClick(i: PostListing) {
1291 let deleteForm: DeletePost = {
1292 post_id: i.props.post_view.post.id,
1293 deleted: !i.props.post_view.post.deleted,
1296 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1299 handleSavePostClick(i: PostListing) {
1301 i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1302 let form: SavePost = {
1303 post_id: i.props.post_view.post.id,
1308 WebSocketService.Instance.send(wsClient.savePost(form));
1311 get crossPostParams(): string {
1312 let post = this.props.post_view.post;
1313 let params = `?title=${encodeURIComponent(post.name)}`;
1316 params += `&url=${encodeURIComponent(post.url)}`;
1319 params += `&body=${encodeURIComponent(this.crossPostBody())}`;
1324 crossPostBody(): string {
1325 let post = this.props.post_view.post;
1326 let body = `${i18n.t("cross_posted_from")} ${
1328 }\n\n${post.body.replace(/^/gm, "> ")}`;
1332 get showBody(): boolean {
1333 return this.props.showBody || this.state.showBody;
1336 handleModRemoveShow(i: PostListing) {
1337 i.state.showRemoveDialog = true;
1338 i.setState(i.state);
1341 handleModRemoveReasonChange(i: PostListing, event: any) {
1342 i.state.removeReason = event.target.value;
1343 i.setState(i.state);
1346 handleModRemoveDataChange(i: PostListing, event: any) {
1347 i.state.removeData = event.target.checked;
1348 i.setState(i.state);
1351 handleModRemoveSubmit(i: PostListing, event: any) {
1352 event.preventDefault();
1353 let form: RemovePost = {
1354 post_id: i.props.post_view.post.id,
1355 removed: !i.props.post_view.post.removed,
1356 reason: i.state.removeReason,
1359 WebSocketService.Instance.send(wsClient.removePost(form));
1361 i.state.showRemoveDialog = false;
1362 i.setState(i.state);
1365 handleModLock(i: PostListing) {
1366 let form: LockPost = {
1367 post_id: i.props.post_view.post.id,
1368 locked: !i.props.post_view.post.locked,
1371 WebSocketService.Instance.send(wsClient.lockPost(form));
1374 handleModSticky(i: PostListing) {
1375 let form: StickyPost = {
1376 post_id: i.props.post_view.post.id,
1377 stickied: !i.props.post_view.post.stickied,
1380 WebSocketService.Instance.send(wsClient.stickyPost(form));
1383 handleModBanFromCommunityShow(i: PostListing) {
1384 i.state.showBanDialog = true;
1385 i.state.banType = BanType.Community;
1386 i.setState(i.state);
1389 handleModBanShow(i: PostListing) {
1390 i.state.showBanDialog = true;
1391 i.state.banType = BanType.Site;
1392 i.setState(i.state);
1395 handleModBanReasonChange(i: PostListing, event: any) {
1396 i.state.banReason = event.target.value;
1397 i.setState(i.state);
1400 handleModBanExpiresChange(i: PostListing, event: any) {
1401 i.state.banExpires = event.target.value;
1402 i.setState(i.state);
1405 handleModBanFromCommunitySubmit(i: PostListing) {
1406 i.state.banType = BanType.Community;
1407 i.setState(i.state);
1408 i.handleModBanBothSubmit(i);
1411 handleModBanSubmit(i: PostListing) {
1412 i.state.banType = BanType.Site;
1413 i.setState(i.state);
1414 i.handleModBanBothSubmit(i);
1417 handleModBanBothSubmit(i: PostListing, event?: any) {
1418 if (event) event.preventDefault();
1420 if (i.state.banType == BanType.Community) {
1421 // If its an unban, restore all their data
1422 let ban = !i.props.post_view.creator_banned_from_community;
1424 i.state.removeData = false;
1426 let form: BanFromCommunity = {
1427 person_id: i.props.post_view.creator.id,
1428 community_id: i.props.post_view.community.id,
1430 remove_data: i.state.removeData,
1431 reason: i.state.banReason,
1432 expires: getUnixTime(i.state.banExpires),
1435 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1437 // If its an unban, restore all their data
1438 let ban = !i.props.post_view.creator.banned;
1440 i.state.removeData = false;
1442 let form: BanPerson = {
1443 person_id: i.props.post_view.creator.id,
1445 remove_data: i.state.removeData,
1446 reason: i.state.banReason,
1447 expires: getUnixTime(i.state.banExpires),
1450 WebSocketService.Instance.send(wsClient.banPerson(form));
1453 i.state.showBanDialog = false;
1454 i.setState(i.state);
1457 handleAddModToCommunity(i: PostListing) {
1458 let form: AddModToCommunity = {
1459 person_id: i.props.post_view.creator.id,
1460 community_id: i.props.post_view.community.id,
1464 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1465 i.setState(i.state);
1468 handleAddAdmin(i: PostListing) {
1469 let form: AddAdmin = {
1470 person_id: i.props.post_view.creator.id,
1474 WebSocketService.Instance.send(wsClient.addAdmin(form));
1475 i.setState(i.state);
1478 handleShowConfirmTransferCommunity(i: PostListing) {
1479 i.state.showConfirmTransferCommunity = true;
1480 i.setState(i.state);
1483 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1484 i.state.showConfirmTransferCommunity = false;
1485 i.setState(i.state);
1488 handleTransferCommunity(i: PostListing) {
1489 let form: TransferCommunity = {
1490 community_id: i.props.post_view.community.id,
1491 person_id: i.props.post_view.creator.id,
1494 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1495 i.state.showConfirmTransferCommunity = false;
1496 i.setState(i.state);
1499 handleShowConfirmTransferSite(i: PostListing) {
1500 i.state.showConfirmTransferSite = true;
1501 i.setState(i.state);
1504 handleCancelShowConfirmTransferSite(i: PostListing) {
1505 i.state.showConfirmTransferSite = false;
1506 i.setState(i.state);
1509 handleTransferSite(i: PostListing) {
1510 let form: TransferSite = {
1511 person_id: i.props.post_view.creator.id,
1514 WebSocketService.Instance.send(wsClient.transferSite(form));
1515 i.state.showConfirmTransferSite = false;
1516 i.setState(i.state);
1519 handleImageExpandClick(i: PostListing) {
1520 i.state.imageExpanded = !i.state.imageExpanded;
1521 i.setState(i.state);
1524 handleViewSource(i: PostListing) {
1525 i.state.viewSource = !i.state.viewSource;
1526 i.setState(i.state);
1529 handleShowAdvanced(i: PostListing) {
1530 i.state.showAdvanced = !i.state.showAdvanced;
1531 i.setState(i.state);
1535 handleShowMoreMobile(i: PostListing) {
1536 i.state.showMoreMobile = !i.state.showMoreMobile;
1537 i.state.showAdvanced = !i.state.showAdvanced;
1538 i.setState(i.state);
1542 handleShowBody(i: PostListing) {
1543 i.state.showBody = !i.state.showBody;
1544 i.setState(i.state);
1548 get pointsTippy(): string {
1549 let points = i18n.t("number_of_points", {
1550 count: this.state.score,
1553 let upvotes = i18n.t("number_of_upvotes", {
1554 count: this.state.upvotes,
1557 let downvotes = i18n.t("number_of_downvotes", {
1558 count: this.state.downvotes,
1561 return `${points} • ${upvotes} • ${downvotes}`;