1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
9 CommunityModeratorView,
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";
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";
50 interface PostListingState {
52 showRemoveDialog: boolean;
54 showBanDialog: boolean;
59 showConfirmTransferSite: boolean;
60 showConfirmTransferCommunity: boolean;
61 imageExpanded: boolean;
63 showAdvanced: boolean;
64 showMoreMobile: boolean;
66 showReportDialog: boolean;
74 interface PostListingProps {
76 duplicates?: PostView[];
77 showCommunity?: boolean;
79 moderators?: CommunityModeratorView[];
80 admins?: PersonViewSafe[];
81 enableDownvotes: boolean;
85 export class PostListing extends Component<PostListingProps, PostListingState> {
86 private emptyState: PostListingState = {
88 showRemoveDialog: false,
94 banType: BanType.Community,
95 showConfirmTransferSite: false,
96 showConfirmTransferCommunity: false,
100 showMoreMobile: false,
102 showReportDialog: false,
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,
110 constructor(props: any, context: any) {
111 super(props, context);
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);
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;
128 this.setState(this.state);
134 {!this.state.showEdit ? (
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}
155 let post = this.props.post_view.post;
159 {post.url && this.showBody && post.embed_title && (
160 <MetadataCard post={post} />
164 (this.state.viewSource ? (
165 <pre>{post.body}</pre>
169 dangerouslySetInnerHTML={mdToHtml(post.body)}
177 imgThumb(src: string) {
178 let post_view = this.props.post_view;
184 nsfw={post_view.post.nsfw || post_view.community.nsfw}
189 getImageSrc(): string {
190 let post = this.props.post_view.post;
191 if (isImage(post.url)) {
192 if (post.url.includes("pictrs")) {
194 } else if (post.thumbnail_url) {
195 return post.thumbnail_url;
199 } else if (post.thumbnail_url) {
200 return post.thumbnail_url;
207 let post = this.props.post_view.post;
209 if (isImage(post.url)) {
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")}
218 {this.imgThumb(this.getImageSrc())}
219 <Icon icon="image" classes="mini-overlay" />
222 } else if (post.thumbnail_url) {
225 class="float-right text-body d-inline-block position-relative mb-2"
230 {this.imgThumb(this.getImageSrc())}
231 <Icon icon="external-link" classes="mini-overlay" />
234 } else if (post.url) {
235 if (isVideo(post.url)) {
237 <div class="embed-responsive embed-responsive-16by9">
243 class="embed-responsive-item"
245 <source src={post.url} type="video/mp4" />
252 className="text-body"
257 <div class="thumbnail rounded bg-light d-flex justify-content-center">
258 <Icon icon="external-link" classes="d-flex align-items-center" />
266 className="text-body"
267 to={`/post/${post.id}`}
268 title={i18n.t("comments")}
270 <div class="thumbnail rounded bg-light d-flex justify-content-center">
271 <Icon icon="message-square" classes="d-flex align-items-center" />
279 let post_view = this.props.post_view;
281 <ul class="list-inline mb-1 text-muted small">
282 <li className="list-inline-item">
283 <PersonListing person={post_view.creator} />
286 <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
289 <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
291 {post_view.creator.bot_account && (
292 <span className="mx-1 badge badge-light">
293 {i18n.t("bot_account").toLowerCase()}
296 {(post_view.creator_banned_from_community ||
297 post_view.creator.banned) && (
298 <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
300 {post_view.creator_blocked && (
301 <span className="mx-1 badge badge-danger">{"blocked"}</span>
303 {this.props.showCommunity && (
305 <span class="mx-1"> {i18n.t("to")} </span>
306 <CommunityLink community={post_view.community} />
310 <li className="list-inline-item">•</li>
311 {post_view.post.url && !(hostname(post_view.post.url) == externalHost) && (
313 <li className="list-inline-item">
315 className="text-muted font-italic"
316 href={post_view.post.url}
317 title={post_view.post.url}
320 {hostname(post_view.post.url)}
323 <li className="list-inline-item">•</li>
326 <li className="list-inline-item">
328 <MomentTime data={post_view.post} />
331 {post_view.post.body && (
333 <li className="list-inline-item">•</li>
334 <li className="list-inline-item">
336 className="text-muted btn btn-sm btn-link p-0"
337 data-tippy-content={md.render(
338 previewLines(post_view.post.body)
340 data-tippy-allowHtml={true}
341 onClick={linkEvent(this, this.handleShowBody)}
343 <Icon icon="book-open" classes="icon-inline mr-1" />
354 <div className={`vote-bar col-1 pr-0 small text-center`}>
356 className={`btn-animate btn btn-link p-0 ${
357 this.state.my_vote == 1 ? "text-info" : "text-muted"
359 onClick={linkEvent(this, this.handlePostLike)}
360 data-tippy-content={i18n.t("upvote")}
361 aria-label={i18n.t("upvote")}
363 <Icon icon="arrow-up1" classes="upvote" />
367 class={`unselectable pointer font-weight-bold text-muted px-1`}
368 data-tippy-content={this.pointsTippy}
370 {numToSI(this.state.score)}
373 <div class="p-1"></div>
375 {this.props.enableDownvotes && (
377 className={`btn-animate btn btn-link p-0 ${
378 this.state.my_vote == -1 ? "text-danger" : "text-muted"
380 onClick={linkEvent(this, this.handlePostDisLike)}
381 data-tippy-content={i18n.t("downvote")}
382 aria-label={i18n.t("downvote")}
384 <Icon icon="arrow-down1" classes="downvote" />
392 let post = this.props.post_view.post;
394 <div className="post-title overflow-hidden">
396 {this.showBody && post.url ? (
398 className={!post.stickied ? "text-body" : "text-primary"}
407 className={!post.stickied ? "text-body" : "text-primary"}
408 to={`/post/${post.id}`}
409 title={i18n.t("comments")}
414 {(isImage(post.url) || post.thumbnail_url) &&
415 (!this.state.imageExpanded ? (
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)}
421 <Icon icon="plus-square" classes="icon-inline" />
426 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
427 onClick={linkEvent(this, this.handleImageExpandClick)}
429 <Icon icon="minus-square" classes="icon-inline" />
433 href={this.getImageSrc()}
434 class="btn btn-link d-inline-block"
435 onClick={linkEvent(this, this.handleImageExpandClick)}
437 <PictrsImage src={this.getImageSrc()} />
443 <small className="ml-2 text-muted font-italic">
449 className="unselectable pointer ml-2 text-muted font-italic"
450 data-tippy-content={i18n.t("deleted")}
452 <Icon icon="trash" classes="icon-inline text-danger" />
457 className="unselectable pointer ml-2 text-muted font-italic"
458 data-tippy-content={i18n.t("locked")}
460 <Icon icon="lock" classes="icon-inline text-danger" />
465 className="unselectable pointer ml-2 text-muted font-italic"
466 data-tippy-content={i18n.t("stickied")}
468 <Icon icon="pin" classes="icon-inline text-primary" />
472 <small className="ml-2 text-muted font-italic">
481 commentsLine(mobile = false) {
482 let post_view = this.props.post_view;
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">
487 className="text-muted small"
488 title={i18n.t("number_of_comments", {
489 count: post_view.counts.comments,
490 formattedCount: post_view.counts.comments,
492 to={`/post/${post_view.post.id}?scrollToComments=true`}
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),
503 {this.state.downvotes !== 0 && showScores() && (
505 class="btn text-muted py-0 pr-0"
506 data-tippy-content={this.pointsTippy}
507 aria-label={i18n.t("downvote")}
510 <Icon icon="arrow-down1" classes="icon-inline mr-1" />
511 <span>{numToSI(this.state.downvotes)}</span>
517 class="btn btn-link btn-animate text-muted py-0"
518 onClick={linkEvent(this, this.handleSavePostClick)}
520 post_view.saved ? i18n.t("unsave") : i18n.t("save")
522 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
527 classes={`icon-inline ${post_view.saved && "text-warning"}`}
534 {/* This is an expanding spacer for mobile */}
535 <div className="flex-grow-1"></div>
541 className={`btn-animate btn py-0 px-1 ${
542 this.state.my_vote == 1 ? "text-info" : "text-muted"
544 data-tippy-content={this.pointsTippy}
545 onClick={linkEvent(this, this.handlePostLike)}
546 aria-label={i18n.t("upvote")}
548 <Icon icon="arrow-up1" classes="icon-inline small mr-2" />
549 {numToSI(this.state.upvotes)}
553 className={`btn-animate btn py-0 px-1 ${
554 this.state.my_vote == 1 ? "text-info" : "text-muted"
556 onClick={linkEvent(this, this.handlePostLike)}
557 aria-label={i18n.t("upvote")}
559 <Icon icon="arrow-up1" classes="icon-inline small" />
562 {this.props.enableDownvotes &&
565 className={`ml-2 btn-animate btn py-0 pl-1 ${
566 this.state.my_vote == -1 ? "text-danger" : "text-muted"
568 onClick={linkEvent(this, this.handlePostDisLike)}
569 data-tippy-content={this.pointsTippy}
570 aria-label={i18n.t("downvote")}
572 <Icon icon="arrow-down1" classes="icon-inline small mr-2" />
573 {this.state.downvotes !== 0 && (
574 <span>{numToSI(this.state.downvotes)}</span>
579 className={`ml-2 btn-animate btn py-0 pl-1 ${
580 this.state.my_vote == -1 ? "text-danger" : "text-muted"
582 onClick={linkEvent(this, this.handlePostDisLike)}
583 aria-label={i18n.t("downvote")}
585 <Icon icon="arrow-down1" classes="icon-inline small" />
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")}
594 post_view.saved ? i18n.t("unsave") : i18n.t("save")
599 classes={`icon-inline ${post_view.saved && "text-warning"}`}
603 {!this.state.showMoreMobile && this.showBody && (
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")}
610 <Icon icon="more-vertical" classes="icon-inline" />
613 {this.state.showMoreMobile && this.postActions(mobile)}
621 let dupes = this.props.duplicates;
624 dupes.length > 0 && (
625 <ul class="list-inline mb-1 small text-muted">
627 <li className="list-inline-item mr-2">
628 {i18n.t("cross_posted_to")}
631 <li className="list-inline-item mr-2">
632 <Link to={`/post/${pv.post.id}`}>
635 : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
645 postActions(mobile = false) {
646 let post_view = this.props.post_view;
648 UserService.Instance.myUserInfo && (
654 class="btn btn-link btn-animate text-muted py-0 pl-0"
655 onClick={linkEvent(this, this.handleSavePostClick)}
657 post_view.saved ? i18n.t("unsave") : i18n.t("save")
660 post_view.saved ? i18n.t("unsave") : i18n.t("save")
665 classes={`icon-inline ${post_view.saved && "text-warning"}`}
670 className="btn btn-link btn-animate text-muted py-0"
671 to={`/create_post${this.crossPostParams}`}
672 title={i18n.t("cross_post")}
674 <Icon icon="copy" classes="icon-inline" />
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")}
684 <Icon icon="flag" classes="icon-inline" />
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")}
692 <Icon icon="slash" classes="icon-inline" />
698 {this.myPost && this.showBody && (
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")}
706 <Icon icon="edit" classes="icon-inline" />
709 class="btn btn-link btn-animate text-muted py-0"
710 onClick={linkEvent(this, this.handleDeleteClick)}
712 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
715 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
720 classes={`icon-inline ${
721 post_view.post.deleted && "text-danger"
728 {!this.state.showAdvanced && this.showBody ? (
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")}
735 <Icon icon="more-vertical" classes="icon-inline" />
739 {this.showBody && post_view.post.body && (
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")}
748 classes={`icon-inline ${
749 this.state.viewSource && "text-success"
754 {this.canModOnSelf && (
757 class="btn btn-link btn-animate text-muted py-0"
758 onClick={linkEvent(this, this.handleModLock)}
760 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
763 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
768 classes={`icon-inline ${
769 post_view.post.locked && "text-danger"
774 class="btn btn-link btn-animate text-muted py-0"
775 onClick={linkEvent(this, this.handleModSticky)}
777 post_view.post.stickied
782 post_view.post.stickied
789 classes={`icon-inline ${
790 post_view.post.stickied && "text-success"
796 {/* Mods can ban from community, and appoint as mods to community */}
797 {(this.canMod || this.canAdmin) &&
798 (!post_view.post.removed ? (
800 class="btn btn-link btn-animate text-muted py-0"
801 onClick={linkEvent(this, this.handleModRemoveShow)}
802 aria-label={i18n.t("remove")}
808 class="btn btn-link btn-animate text-muted py-0"
809 onClick={linkEvent(this, this.handleModRemoveSubmit)}
810 aria-label={i18n.t("restore")}
818 (!post_view.creator_banned_from_community ? (
820 class="btn btn-link btn-animate text-muted py-0"
823 this.handleModBanFromCommunityShow
825 aria-label={i18n.t("ban")}
831 class="btn btn-link btn-animate text-muted py-0"
834 this.handleModBanFromCommunitySubmit
836 aria-label={i18n.t("unban")}
841 {!post_view.creator_banned_from_community && (
843 class="btn btn-link btn-animate text-muted py-0"
844 onClick={linkEvent(this, this.handleAddModToCommunity)}
847 ? i18n.t("remove_as_mod")
848 : i18n.t("appoint_as_mod")
852 ? i18n.t("remove_as_mod")
853 : i18n.t("appoint_as_mod")}
858 {/* Community creators and admins can transfer community to another mod */}
859 {(this.amCommunityCreator || this.canAdmin) &&
861 (!this.state.showConfirmTransferCommunity ? (
863 class="btn btn-link btn-animate text-muted py-0"
866 this.handleShowConfirmTransferCommunity
868 aria-label={i18n.t("transfer_community")}
870 {i18n.t("transfer_community")}
875 class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
876 aria-label={i18n.t("are_you_sure")}
878 {i18n.t("are_you_sure")}
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)}
888 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
891 this.handleCancelShowConfirmTransferCommunity
893 aria-label={i18n.t("no")}
899 {/* Admins can ban from all, and appoint other admins */}
903 (!post_view.creator.banned ? (
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")}
909 {i18n.t("ban_from_site")}
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")}
917 {i18n.t("unban_from_site")}
920 {!post_view.creator.banned && post_view.creator.local && (
922 class="btn btn-link btn-animate text-muted py-0"
923 onClick={linkEvent(this, this.handleAddAdmin)}
926 ? i18n.t("remove_as_admin")
927 : i18n.t("appoint_as_admin")
931 ? i18n.t("remove_as_admin")
932 : i18n.t("appoint_as_admin")}
937 {/* Site Creator can transfer to another admin */}
938 {this.amSiteCreator &&
940 (!this.state.showConfirmTransferSite ? (
942 class="btn btn-link btn-animate text-muted py-0"
945 this.handleShowConfirmTransferSite
947 aria-label={i18n.t("transfer_site")}
949 {i18n.t("transfer_site")}
954 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
955 aria-label={i18n.t("are_you_sure")}
957 {i18n.t("are_you_sure")}
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")}
967 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
970 this.handleCancelShowConfirmTransferSite
972 aria-label={i18n.t("no")}
985 removeAndBanDialogs() {
986 let post = this.props.post_view;
989 {this.state.showRemoveDialog && (
992 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
994 <label class="sr-only" htmlFor="post-listing-remove-reason">
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)}
1007 class="btn btn-secondary"
1008 aria-label={i18n.t("remove_post")}
1010 {i18n.t("remove_post")}
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">
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)}
1028 <div class="form-group">
1029 <div class="form-check">
1031 class="form-check-input"
1032 id="mod-ban-remove-data"
1034 checked={this.state.removeData}
1035 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1038 class="form-check-label"
1039 htmlFor="mod-ban-remove-data"
1040 title={i18n.t("remove_content_more")}
1042 {i18n.t("remove_content")}
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)} /> */}
1052 <div class="form-group row">
1055 class="btn btn-secondary"
1056 aria-label={i18n.t("ban")}
1058 {i18n.t("ban")} {post.creator.name}
1063 {this.state.showReportDialog && (
1066 onSubmit={linkEvent(this, this.handleReportSubmit)}
1068 <label class="sr-only" htmlFor="post-report-reason">
1073 id="post-report-reason"
1074 class="form-control mr-2"
1075 placeholder={i18n.t("reason")}
1077 value={this.state.reportReason}
1078 onInput={linkEvent(this, this.handleReportReasonChange)}
1082 class="btn btn-secondary"
1083 aria-label={i18n.t("create_report")}
1085 {i18n.t("create_report")}
1094 let post = this.props.post_view.post;
1095 return post.thumbnail_url || isImage(post.url) ? (
1097 <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1098 {this.postTitleLine()}
1101 {/* Post body prev or thumbnail */}
1102 {!this.state.imageExpanded && this.thumbnail()}
1106 this.postTitleLine()
1110 showMobilePreview() {
1111 let post = this.props.post_view.post;
1116 className="md-div mb-1"
1117 dangerouslySetInnerHTML={{
1118 __html: md.render(previewLines(post.body)),
1128 {/* The mobile view*/}
1129 <div class="d-block d-sm-none">
1131 <div class="col-12">
1132 {this.createdLine()}
1134 {/* If it has a thumbnail, do a right aligned thumbnail */}
1135 {this.mobileThumbnail()}
1137 {/* Show a preview of the post body */}
1138 {this.showMobilePreview()}
1140 {this.commentsLine(true)}
1141 {this.duplicatesLine()}
1142 {this.removeAndBanDialogs()}
1147 {/* The larger view*/}
1148 <div class="d-none d-sm-block">
1151 {!this.state.imageExpanded && (
1152 <div class="col-sm-2 pr-0">
1153 <div class="">{this.thumbnail()}</div>
1158 this.state.imageExpanded ? "col-12" : "col-12 col-sm-9"
1162 <div className="col-12">
1163 {this.postTitleLine()}
1164 {this.createdLine()}
1165 {this.commentsLine()}
1166 {this.duplicatesLine()}
1167 {this.postActions()}
1168 {this.removeAndBanDialogs()}
1178 private get myPost(): boolean {
1180 UserService.Instance.myUserInfo &&
1181 this.props.post_view.creator.id ==
1182 UserService.Instance.myUserInfo.local_user_view.person.id
1186 get isMod(): boolean {
1188 this.props.moderators &&
1190 this.props.moderators.map(m => m.moderator.id),
1191 this.props.post_view.creator.id
1196 get isAdmin(): boolean {
1198 this.props.admins &&
1200 this.props.admins.map(a => a.person.id),
1201 this.props.post_view.creator.id
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));
1213 UserService.Instance.myUserInfo,
1215 this.props.post_view.creator.id
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));
1229 UserService.Instance.myUserInfo,
1231 this.props.post_view.creator.id,
1239 get canAdmin(): boolean {
1241 this.props.admins &&
1243 UserService.Instance.myUserInfo,
1244 this.props.admins.map(a => a.person.id),
1245 this.props.post_view.creator.id
1250 get amCommunityCreator(): boolean {
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
1261 get amSiteCreator(): boolean {
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
1272 handlePostLike(i: PostListing, event: any) {
1273 event.preventDefault();
1274 if (!UserService.Instance.myUserInfo) {
1275 this.context.router.history.push(`/login`);
1278 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1280 if (i.state.my_vote == 1) {
1283 } else if (i.state.my_vote == -1) {
1284 i.state.downvotes--;
1292 i.state.my_vote = new_vote;
1294 let form: CreatePostLike = {
1295 post_id: i.props.post_view.post.id,
1296 score: i.state.my_vote,
1300 WebSocketService.Instance.send(wsClient.likePost(form));
1301 i.setState(i.state);
1305 handlePostDisLike(i: PostListing, event: any) {
1306 event.preventDefault();
1307 if (!UserService.Instance.myUserInfo) {
1308 this.context.router.history.push(`/login`);
1311 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1313 if (i.state.my_vote == 1) {
1316 i.state.downvotes++;
1317 } else if (i.state.my_vote == -1) {
1318 i.state.downvotes--;
1321 i.state.downvotes++;
1325 i.state.my_vote = new_vote;
1327 let form: CreatePostLike = {
1328 post_id: i.props.post_view.post.id,
1329 score: i.state.my_vote,
1333 WebSocketService.Instance.send(wsClient.likePost(form));
1334 i.setState(i.state);
1338 handleEditClick(i: PostListing) {
1339 i.state.showEdit = true;
1340 i.setState(i.state);
1343 handleEditCancel() {
1344 this.state.showEdit = false;
1345 this.setState(this.state);
1348 // The actual editing is done in the recieve for post
1350 this.state.showEdit = false;
1351 this.setState(this.state);
1354 handleShowReportDialog(i: PostListing) {
1355 i.state.showReportDialog = !i.state.showReportDialog;
1356 i.setState(this.state);
1359 handleReportReasonChange(i: PostListing, event: any) {
1360 i.state.reportReason = event.target.value;
1361 i.setState(i.state);
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,
1371 WebSocketService.Instance.send(wsClient.createPostReport(form));
1373 i.state.showReportDialog = false;
1374 i.setState(i.state);
1377 handleBlockUserClick(i: PostListing) {
1378 let blockUserForm: BlockPerson = {
1379 person_id: i.props.post_view.creator.id,
1383 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
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,
1392 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1395 handleSavePostClick(i: PostListing) {
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,
1404 WebSocketService.Instance.send(wsClient.savePost(form));
1407 get crossPostParams(): string {
1408 let post = this.props.post_view.post;
1409 let params = `?title=${encodeURIComponent(post.name)}`;
1412 params += `&url=${encodeURIComponent(post.url)}`;
1415 params += `&body=${encodeURIComponent(this.crossPostBody())}`;
1420 crossPostBody(): string {
1421 let post = this.props.post_view.post;
1422 let body = `${i18n.t("cross_posted_from")} ${
1424 }\n\n${post.body.replace(/^/gm, "> ")}`;
1428 get showBody(): boolean {
1429 return this.props.showBody || this.state.showBody;
1432 handleModRemoveShow(i: PostListing) {
1433 i.state.showRemoveDialog = !i.state.showRemoveDialog;
1434 i.state.showBanDialog = false;
1435 i.setState(i.state);
1438 handleModRemoveReasonChange(i: PostListing, event: any) {
1439 i.state.removeReason = event.target.value;
1440 i.setState(i.state);
1443 handleModRemoveDataChange(i: PostListing, event: any) {
1444 i.state.removeData = event.target.checked;
1445 i.setState(i.state);
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,
1456 WebSocketService.Instance.send(wsClient.removePost(form));
1458 i.state.showRemoveDialog = false;
1459 i.setState(i.state);
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,
1468 WebSocketService.Instance.send(wsClient.lockPost(form));
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,
1477 WebSocketService.Instance.send(wsClient.stickyPost(form));
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);
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);
1494 handleModBanReasonChange(i: PostListing, event: any) {
1495 i.state.banReason = event.target.value;
1496 i.setState(i.state);
1499 handleModBanExpiresChange(i: PostListing, event: any) {
1500 i.state.banExpires = event.target.value;
1501 i.setState(i.state);
1504 handleModBanFromCommunitySubmit(i: PostListing) {
1505 i.state.banType = BanType.Community;
1506 i.setState(i.state);
1507 i.handleModBanBothSubmit(i);
1510 handleModBanSubmit(i: PostListing) {
1511 i.state.banType = BanType.Site;
1512 i.setState(i.state);
1513 i.handleModBanBothSubmit(i);
1516 handleModBanBothSubmit(i: PostListing, event?: any) {
1517 if (event) event.preventDefault();
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;
1523 i.state.removeData = false;
1525 let form: BanFromCommunity = {
1526 person_id: i.props.post_view.creator.id,
1527 community_id: i.props.post_view.community.id,
1529 remove_data: i.state.removeData,
1530 reason: i.state.banReason,
1531 expires: getUnixTime(i.state.banExpires),
1534 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1536 // If its an unban, restore all their data
1537 let ban = !i.props.post_view.creator.banned;
1539 i.state.removeData = false;
1541 let form: BanPerson = {
1542 person_id: i.props.post_view.creator.id,
1544 remove_data: i.state.removeData,
1545 reason: i.state.banReason,
1546 expires: getUnixTime(i.state.banExpires),
1549 WebSocketService.Instance.send(wsClient.banPerson(form));
1552 i.state.showBanDialog = false;
1553 i.setState(i.state);
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,
1563 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1564 i.setState(i.state);
1567 handleAddAdmin(i: PostListing) {
1568 let form: AddAdmin = {
1569 person_id: i.props.post_view.creator.id,
1573 WebSocketService.Instance.send(wsClient.addAdmin(form));
1574 i.setState(i.state);
1577 handleShowConfirmTransferCommunity(i: PostListing) {
1578 i.state.showConfirmTransferCommunity = true;
1579 i.setState(i.state);
1582 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1583 i.state.showConfirmTransferCommunity = false;
1584 i.setState(i.state);
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,
1593 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1594 i.state.showConfirmTransferCommunity = false;
1595 i.setState(i.state);
1598 handleShowConfirmTransferSite(i: PostListing) {
1599 i.state.showConfirmTransferSite = true;
1600 i.setState(i.state);
1603 handleCancelShowConfirmTransferSite(i: PostListing) {
1604 i.state.showConfirmTransferSite = false;
1605 i.setState(i.state);
1608 handleTransferSite(i: PostListing) {
1609 let form: TransferSite = {
1610 person_id: i.props.post_view.creator.id,
1613 WebSocketService.Instance.send(wsClient.transferSite(form));
1614 i.state.showConfirmTransferSite = false;
1615 i.setState(i.state);
1618 handleImageExpandClick(i: PostListing, event: any) {
1619 event.preventDefault();
1620 i.state.imageExpanded = !i.state.imageExpanded;
1621 i.setState(i.state);
1624 handleViewSource(i: PostListing) {
1625 i.state.viewSource = !i.state.viewSource;
1626 i.setState(i.state);
1629 handleShowAdvanced(i: PostListing) {
1630 i.state.showAdvanced = !i.state.showAdvanced;
1631 i.setState(i.state);
1635 handleShowMoreMobile(i: PostListing) {
1636 i.state.showMoreMobile = !i.state.showMoreMobile;
1637 i.state.showAdvanced = !i.state.showAdvanced;
1638 i.setState(i.state);
1642 handleShowBody(i: PostListing) {
1643 i.state.showBody = !i.state.showBody;
1644 i.setState(i.state);
1648 get pointsTippy(): string {
1649 let points = i18n.t("number_of_points", {
1650 count: this.state.score,
1651 formattedCount: this.state.score,
1654 let upvotes = i18n.t("number_of_upvotes", {
1655 count: this.state.upvotes,
1656 formattedCount: this.state.upvotes,
1659 let downvotes = i18n.t("number_of_downvotes", {
1660 count: this.state.downvotes,
1661 formattedCount: this.state.downvotes,
1664 return `${points} • ${upvotes} • ${downvotes}`;