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";
11 CommunityModeratorView,
25 } from "lemmy-js-client";
26 import { externalHost } from "../../env";
27 import { i18n } from "../../i18next";
28 import { BanType, PurgeType } from "../../interfaces";
29 import { UserService, WebSocketService } from "../../services";
50 import { Icon, PurgeWarning, Spinner } from "../common/icon";
51 import { MomentTime } from "../common/moment-time";
52 import { PictrsImage } from "../common/pictrs-image";
53 import { CommunityLink } from "../community/community-link";
54 import { PersonListing } from "../person/person-listing";
55 import { MetadataCard } from "./metadata-card";
56 import { PostForm } from "./post-form";
58 interface PostListingState {
60 showRemoveDialog: boolean;
61 showPurgeDialog: boolean;
62 purgeReason: Option<string>;
64 purgeLoading: boolean;
65 removeReason: Option<string>;
66 showBanDialog: boolean;
67 banReason: Option<string>;
68 banExpireDays: Option<number>;
71 showConfirmTransferSite: boolean;
72 showConfirmTransferCommunity: boolean;
73 imageExpanded: boolean;
75 showAdvanced: boolean;
76 showMoreMobile: boolean;
78 showReportDialog: boolean;
79 reportReason: Option<string>;
80 my_vote: Option<number>;
86 interface PostListingProps {
88 duplicates: Option<PostView[]>;
89 moderators: Option<CommunityModeratorView[]>;
90 admins: Option<PersonViewSafe[]>;
91 showCommunity?: boolean;
93 enableDownvotes?: boolean;
98 export class PostListing extends Component<PostListingProps, PostListingState> {
99 private emptyState: PostListingState = {
101 showRemoveDialog: false,
102 showPurgeDialog: false,
104 purgeType: PurgeType.Person,
107 showBanDialog: false,
110 banType: BanType.Community,
112 showConfirmTransferSite: false,
113 showConfirmTransferCommunity: false,
114 imageExpanded: false,
117 showMoreMobile: false,
119 showReportDialog: false,
121 my_vote: this.props.post_view.my_vote,
122 score: this.props.post_view.counts.score,
123 upvotes: this.props.post_view.counts.upvotes,
124 downvotes: this.props.post_view.counts.downvotes,
127 constructor(props: any, context: any) {
128 super(props, context);
130 this.state = this.emptyState;
131 this.handlePostLike = this.handlePostLike.bind(this);
132 this.handlePostDisLike = this.handlePostDisLike.bind(this);
133 this.handleEditPost = this.handleEditPost.bind(this);
134 this.handleEditCancel = this.handleEditCancel.bind(this);
137 componentWillReceiveProps(nextProps: PostListingProps) {
138 this.state.my_vote = nextProps.post_view.my_vote;
139 this.state.upvotes = nextProps.post_view.counts.upvotes;
140 this.state.downvotes = nextProps.post_view.counts.downvotes;
141 this.state.score = nextProps.post_view.counts.score;
142 if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
143 this.state.imageExpanded = false;
145 this.setState(this.state);
149 let post = this.props.post_view.post;
151 <div class="post-listing">
152 {!this.state.showEdit ? (
155 {this.state.imageExpanded && this.img}
156 {post.url.isSome() &&
158 post.embed_title.isSome() && <MetadataCard post={post} />}
159 {this.showBody && this.body()}
164 post_view={Some(this.props.post_view)}
167 onEdit={this.handleEditPost}
168 onCancel={this.handleEditCancel}
169 enableNsfw={this.props.enableNsfw}
170 enableDownvotes={this.props.enableDownvotes}
179 return this.props.post_view.post.body.match({
181 <div class="col-12 card my-2 p-2">
182 {this.state.viewSource ? (
185 <div className="md-div" dangerouslySetInnerHTML={mdToHtml(body)} />
194 return this.imageSrc.match({
197 <div class="offset-sm-3 my-2 d-none d-sm-block">
198 <a href={src} class="d-inline-block">
199 <PictrsImage src={src} />
202 <div className="my-2 d-block d-sm-none">
204 class="d-inline-block"
205 onClick={linkEvent(this, this.handleImageExpandClick)}
207 <PictrsImage src={src} />
216 imgThumb(src: string) {
217 let post_view = this.props.post_view;
223 nsfw={post_view.post.nsfw || post_view.community.nsfw}
228 get imageSrc(): Option<string> {
229 let post = this.props.post_view.post;
231 let thumbnail = post.thumbnail_url;
233 if (url.isSome() && isImage(url.unwrap())) {
234 if (url.unwrap().includes("pictrs")) {
236 } else if (thumbnail.isSome()) {
241 } else if (thumbnail.isSome()) {
249 let post = this.props.post_view.post;
251 let thumbnail = post.thumbnail_url;
253 if (url.isSome() && isImage(url.unwrap())) {
256 href={this.imageSrc.unwrap()}
257 class="text-body d-inline-block position-relative mb-2"
258 data-tippy-content={i18n.t("expand_here")}
259 onClick={linkEvent(this, this.handleImageExpandClick)}
260 aria-label={i18n.t("expand_here")}
262 {this.imgThumb(this.imageSrc.unwrap())}
263 <Icon icon="image" classes="mini-overlay" />
266 } else if (url.isSome() && thumbnail.isSome()) {
269 class="text-body d-inline-block position-relative mb-2"
274 {this.imgThumb(this.imageSrc.unwrap())}
275 <Icon icon="external-link" classes="mini-overlay" />
278 } else if (url.isSome()) {
279 if (isVideo(url.unwrap())) {
281 <div class="embed-responsive embed-responsive-16by9">
287 class="embed-responsive-item"
289 <source src={url.unwrap()} type="video/mp4" />
296 className="text-body"
301 <div class="thumbnail rounded bg-light d-flex justify-content-center">
302 <Icon icon="external-link" classes="d-flex align-items-center" />
310 className="text-body"
311 to={`/post/${post.id}`}
312 title={i18n.t("comments")}
314 <div class="thumbnail rounded bg-light d-flex justify-content-center">
315 <Icon icon="message-square" classes="d-flex align-items-center" />
323 let post_view = this.props.post_view;
325 <ul class="list-inline mb-1 text-muted small">
326 <li className="list-inline-item">
327 <PersonListing person={post_view.creator} />
329 {this.creatorIsMod_ && (
330 <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
332 {this.creatorIsAdmin_ && (
333 <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
335 {post_view.creator.bot_account && (
336 <span className="mx-1 badge badge-light">
337 {i18n.t("bot_account").toLowerCase()}
340 {(post_view.creator_banned_from_community ||
341 isBanned(post_view.creator)) && (
342 <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
344 {post_view.creator_blocked && (
345 <span className="mx-1 badge badge-danger">{"blocked"}</span>
347 {this.props.showCommunity && (
349 <span class="mx-1"> {i18n.t("to")} </span>
350 <CommunityLink community={post_view.community} />
354 <li className="list-inline-item">•</li>
355 {post_view.post.url.match({
357 !(hostname(url) == externalHost) && (
359 <li className="list-inline-item">
361 className="text-muted font-italic"
369 <li className="list-inline-item">•</li>
374 <li className="list-inline-item">
377 published={post_view.post.published}
378 updated={post_view.post.updated}
382 {post_view.post.body.match({
385 <li className="list-inline-item">•</li>
386 <li className="list-inline-item">
388 className="text-muted btn btn-sm btn-link p-0"
389 data-tippy-content={md.render(body)}
390 data-tippy-allowHtml={true}
391 onClick={linkEvent(this, this.handleShowBody)}
393 <Icon icon="book-open" classes="icon-inline mr-1" />
406 <div className={`vote-bar col-1 pr-0 small text-center`}>
408 className={`btn-animate btn btn-link p-0 ${
409 this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted"
411 onClick={linkEvent(this, this.handlePostLike)}
412 data-tippy-content={i18n.t("upvote")}
413 aria-label={i18n.t("upvote")}
415 <Icon icon="arrow-up1" classes="upvote" />
419 class={`unselectable pointer font-weight-bold text-muted px-1`}
420 data-tippy-content={this.pointsTippy}
422 {numToSI(this.state.score)}
425 <div class="p-1"></div>
427 {this.props.enableDownvotes && (
429 className={`btn-animate btn btn-link p-0 ${
430 this.state.my_vote.unwrapOr(0) == -1
434 onClick={linkEvent(this, this.handlePostDisLike)}
435 data-tippy-content={i18n.t("downvote")}
436 aria-label={i18n.t("downvote")}
438 <Icon icon="arrow-down1" classes="downvote" />
446 let post = this.props.post_view.post;
448 <div className="post-title overflow-hidden">
453 className={!post.stickied ? "text-body" : "text-primary"}
463 className={!post.stickied ? "text-body" : "text-primary"}
464 to={`/post/${post.id}`}
465 title={i18n.t("comments")}
471 {post.url.map(isImage).or(post.thumbnail_url).unwrapOr(false) && (
473 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
474 data-tippy-content={i18n.t("expand_here")}
475 onClick={linkEvent(this, this.handleImageExpandClick)}
479 !this.state.imageExpanded ? "plus-square" : "minus-square"
481 classes="icon-inline"
486 <small className="ml-2 text-muted font-italic">
492 className="unselectable pointer ml-2 text-muted font-italic"
493 data-tippy-content={i18n.t("deleted")}
495 <Icon icon="trash" classes="icon-inline text-danger" />
500 className="unselectable pointer ml-2 text-muted font-italic"
501 data-tippy-content={i18n.t("locked")}
503 <Icon icon="lock" classes="icon-inline text-danger" />
508 className="unselectable pointer ml-2 text-muted font-italic"
509 data-tippy-content={i18n.t("stickied")}
511 <Icon icon="pin" classes="icon-inline text-primary" />
515 <small className="ml-2 text-muted font-italic">
525 return this.props.duplicates.match({
527 dupes.length > 0 && (
528 <ul class="list-inline mb-1 small text-muted">
530 <li className="list-inline-item mr-2">
531 {i18n.t("cross_posted_to")}
534 <li className="list-inline-item mr-2">
535 <Link to={`/post/${pv.post.id}`}>
538 : `${pv.community.name}@${hostname(
539 pv.community.actor_id
551 commentsLine(mobile = false) {
552 let post = this.props.post_view.post;
554 <div class="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1">
555 {this.commentsButton}
558 className="btn btn-link btn-animate text-muted py-0"
559 title={i18n.t("link")}
562 <Icon icon="fedilink" inline />
565 {mobile && !this.props.viewOnly && this.mobileVotes}
566 {UserService.Instance.myUserInfo.isSome() &&
567 !this.props.viewOnly &&
568 this.postActions(mobile)}
573 postActions(mobile = false) {
574 // Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
575 // Possible enhancement: Make each button a component.
576 let post_view = this.props.post_view;
580 {this.crossPostButton}
581 {mobile && this.showMoreButton}
582 {(!mobile || this.state.showAdvanced) && (
590 {this.myPost && (this.showBody || this.state.showAdvanced) && (
598 {this.state.showAdvanced && (
601 post_view.post.body.isSome() &&
602 this.viewSourceButton}
603 {this.canModOnSelf_ && (
609 {(this.canMod_ || this.canAdmin_) && <>{this.modRemoveButton}</>}
612 {!mobile && this.showMoreButton}
617 get commentsButton() {
618 let post_view = this.props.post_view;
620 <button class="btn btn-link text-muted py-0 pl-0">
622 className="text-muted"
623 title={i18n.t("number_of_comments", {
624 count: post_view.counts.comments,
625 formattedCount: post_view.counts.comments,
627 to={`/post/${post_view.post.id}?scrollToComments=true`}
629 <Icon icon="message-square" classes="mr-1" inline />
630 {i18n.t("number_of_comments", {
631 count: post_view.counts.comments,
632 formattedCount: numToSI(post_view.counts.comments),
641 let tippy = showScores() ? { "data-tippy-content": this.pointsTippy } : {};
646 className={`btn-animate btn py-0 px-1 ${
647 this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted"
650 onClick={linkEvent(this, this.handlePostLike)}
651 aria-label={i18n.t("upvote")}
653 <Icon icon="arrow-up1" classes="icon-inline small" />
655 <span class="ml-2">{numToSI(this.state.upvotes)}</span>
658 {this.props.enableDownvotes && (
660 className={`ml-2 btn-animate btn py-0 px-1 ${
661 this.state.my_vote.unwrapOr(0) == -1
665 onClick={linkEvent(this, this.handlePostDisLike)}
667 aria-label={i18n.t("downvote")}
669 <Icon icon="arrow-down1" classes="icon-inline small" />
672 class={classNames("ml-2", {
673 invisible: this.state.downvotes === 0,
676 {numToSI(this.state.downvotes)}
687 let saved = this.props.post_view.saved;
688 let label = saved ? i18n.t("unsave") : i18n.t("save");
691 class="btn btn-link btn-animate text-muted py-0"
692 onClick={linkEvent(this, this.handleSavePostClick)}
693 data-tippy-content={label}
698 classes={classNames({ "text-warning": saved })}
705 get crossPostButton() {
708 className="btn btn-link btn-animate text-muted py-0"
709 to={`/create_post${this.crossPostParams}`}
710 title={i18n.t("cross_post")}
712 <Icon icon="copy" inline />
720 class="btn btn-link btn-animate text-muted py-0"
721 onClick={linkEvent(this, this.handleShowReportDialog)}
722 data-tippy-content={i18n.t("show_report_dialog")}
723 aria-label={i18n.t("show_report_dialog")}
725 <Icon icon="flag" inline />
733 class="btn btn-link btn-animate text-muted py-0"
734 onClick={linkEvent(this, this.handleBlockUserClick)}
735 data-tippy-content={i18n.t("block_user")}
736 aria-label={i18n.t("block_user")}
738 <Icon icon="slash" inline />
746 class="btn btn-link btn-animate text-muted py-0"
747 onClick={linkEvent(this, this.handleEditClick)}
748 data-tippy-content={i18n.t("edit")}
749 aria-label={i18n.t("edit")}
751 <Icon icon="edit" inline />
757 let deleted = this.props.post_view.post.deleted;
758 let label = !deleted ? i18n.t("delete") : i18n.t("restore");
761 class="btn btn-link btn-animate text-muted py-0"
762 onClick={linkEvent(this, this.handleDeleteClick)}
763 data-tippy-content={label}
768 classes={classNames({ "text-danger": deleted })}
775 get showMoreButton() {
778 class="btn btn-link btn-animate text-muted py-0"
779 onClick={linkEvent(this, this.handleShowAdvanced)}
780 data-tippy-content={i18n.t("more")}
781 aria-label={i18n.t("more")}
783 <Icon icon="more-vertical" inline />
788 get viewSourceButton() {
791 class="btn btn-link btn-animate text-muted py-0"
792 onClick={linkEvent(this, this.handleViewSource)}
793 data-tippy-content={i18n.t("view_source")}
794 aria-label={i18n.t("view_source")}
798 classes={classNames({ "text-success": this.state.viewSource })}
806 let locked = this.props.post_view.post.locked;
807 let label = locked ? i18n.t("unlock") : i18n.t("lock");
810 class="btn btn-link btn-animate text-muted py-0"
811 onClick={linkEvent(this, this.handleModLock)}
812 data-tippy-content={label}
817 classes={classNames({ "text-danger": locked })}
825 let stickied = this.props.post_view.post.stickied;
826 let label = stickied ? i18n.t("unsticky") : i18n.t("sticky");
829 class="btn btn-link btn-animate text-muted py-0"
830 onClick={linkEvent(this, this.handleModSticky)}
831 data-tippy-content={label}
836 classes={classNames({ "text-success": stickied })}
843 get modRemoveButton() {
844 let removed = this.props.post_view.post.removed;
847 class="btn btn-link btn-animate text-muted py-0"
850 !removed ? this.handleModRemoveShow : this.handleModRemoveSubmit
853 {/* TODO: Find an icon for this. */}
854 {!removed ? i18n.t("remove") : i18n.t("restore")}
860 * Mod/Admin actions to be taken against the author.
864 let post_view = this.props.post_view;
866 this.state.showAdvanced && (
870 {!this.creatorIsMod_ &&
871 (!post_view.creator_banned_from_community ? (
873 class="btn btn-link btn-animate text-muted py-0"
876 this.handleModBanFromCommunityShow
878 aria-label={i18n.t("ban")}
884 class="btn btn-link btn-animate text-muted py-0"
887 this.handleModBanFromCommunitySubmit
889 aria-label={i18n.t("unban")}
894 {!post_view.creator_banned_from_community && (
896 class="btn btn-link btn-animate text-muted py-0"
897 onClick={linkEvent(this, this.handleAddModToCommunity)}
900 ? i18n.t("remove_as_mod")
901 : i18n.t("appoint_as_mod")
905 ? i18n.t("remove_as_mod")
906 : i18n.t("appoint_as_mod")}
911 {/* Community creators and admins can transfer community to another mod */}
912 {(amCommunityCreator(this.props.moderators, post_view.creator.id) ||
914 this.creatorIsMod_ &&
915 (!this.state.showConfirmTransferCommunity ? (
917 class="btn btn-link btn-animate text-muted py-0"
920 this.handleShowConfirmTransferCommunity
922 aria-label={i18n.t("transfer_community")}
924 {i18n.t("transfer_community")}
929 class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
930 aria-label={i18n.t("are_you_sure")}
932 {i18n.t("are_you_sure")}
935 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
936 aria-label={i18n.t("yes")}
937 onClick={linkEvent(this, this.handleTransferCommunity)}
942 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
945 this.handleCancelShowConfirmTransferCommunity
947 aria-label={i18n.t("no")}
953 {/* Admins can ban from all, and appoint other admins */}
956 {!this.creatorIsAdmin_ && (
958 {!isBanned(post_view.creator) ? (
960 class="btn btn-link btn-animate text-muted py-0"
961 onClick={linkEvent(this, this.handleModBanShow)}
962 aria-label={i18n.t("ban_from_site")}
964 {i18n.t("ban_from_site")}
968 class="btn btn-link btn-animate text-muted py-0"
969 onClick={linkEvent(this, this.handleModBanSubmit)}
970 aria-label={i18n.t("unban_from_site")}
972 {i18n.t("unban_from_site")}
976 class="btn btn-link btn-animate text-muted py-0"
977 onClick={linkEvent(this, this.handlePurgePersonShow)}
978 aria-label={i18n.t("purge_user")}
980 {i18n.t("purge_user")}
983 class="btn btn-link btn-animate text-muted py-0"
984 onClick={linkEvent(this, this.handlePurgePostShow)}
985 aria-label={i18n.t("purge_post")}
987 {i18n.t("purge_post")}
991 {!isBanned(post_view.creator) && post_view.creator.local && (
993 class="btn btn-link btn-animate text-muted py-0"
994 onClick={linkEvent(this, this.handleAddAdmin)}
997 ? i18n.t("remove_as_admin")
998 : i18n.t("appoint_as_admin")
1001 {this.creatorIsAdmin_
1002 ? i18n.t("remove_as_admin")
1003 : i18n.t("appoint_as_admin")}
1013 removeAndBanDialogs() {
1014 let post = this.props.post_view;
1015 let purgeTypeText: string;
1016 if (this.state.purgeType == PurgeType.Post) {
1017 purgeTypeText = i18n.t("purge_post");
1018 } else if (this.state.purgeType == PurgeType.Person) {
1019 purgeTypeText = `${i18n.t("purge")} ${post.creator.name}`;
1023 {this.state.showRemoveDialog && (
1026 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
1028 <label class="sr-only" htmlFor="post-listing-remove-reason">
1033 id="post-listing-remove-reason"
1034 class="form-control mr-2"
1035 placeholder={i18n.t("reason")}
1036 value={toUndefined(this.state.removeReason)}
1037 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1041 class="btn btn-secondary"
1042 aria-label={i18n.t("remove_post")}
1044 {i18n.t("remove_post")}
1048 {this.state.showBanDialog && (
1049 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1050 <div class="form-group row col-12">
1051 <label class="col-form-label" htmlFor="post-listing-ban-reason">
1056 id="post-listing-ban-reason"
1057 class="form-control mr-2"
1058 placeholder={i18n.t("reason")}
1059 value={toUndefined(this.state.banReason)}
1060 onInput={linkEvent(this, this.handleModBanReasonChange)}
1062 <label class="col-form-label" htmlFor={`mod-ban-expires`}>
1067 id={`mod-ban-expires`}
1068 class="form-control mr-2"
1069 placeholder={i18n.t("number_of_days")}
1070 value={toUndefined(this.state.banExpireDays)}
1071 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1073 <div class="form-group">
1074 <div class="form-check">
1076 class="form-check-input"
1077 id="mod-ban-remove-data"
1079 checked={this.state.removeData}
1080 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1083 class="form-check-label"
1084 htmlFor="mod-ban-remove-data"
1085 title={i18n.t("remove_content_more")}
1087 {i18n.t("remove_content")}
1092 {/* TODO hold off on expires until later */}
1093 {/* <div class="form-group row"> */}
1094 {/* <label class="col-form-label">Expires</label> */}
1095 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1097 <div class="form-group row">
1100 class="btn btn-secondary"
1101 aria-label={i18n.t("ban")}
1103 {i18n.t("ban")} {post.creator.name}
1108 {this.state.showReportDialog && (
1111 onSubmit={linkEvent(this, this.handleReportSubmit)}
1113 <label class="sr-only" htmlFor="post-report-reason">
1118 id="post-report-reason"
1119 class="form-control mr-2"
1120 placeholder={i18n.t("reason")}
1122 value={toUndefined(this.state.reportReason)}
1123 onInput={linkEvent(this, this.handleReportReasonChange)}
1127 class="btn btn-secondary"
1128 aria-label={i18n.t("create_report")}
1130 {i18n.t("create_report")}
1134 {this.state.showPurgeDialog && (
1137 onSubmit={linkEvent(this, this.handlePurgeSubmit)}
1140 <label class="sr-only" htmlFor="purge-reason">
1146 class="form-control mr-2"
1147 placeholder={i18n.t("reason")}
1148 value={toUndefined(this.state.purgeReason)}
1149 onInput={linkEvent(this, this.handlePurgeReasonChange)}
1151 {this.state.purgeLoading ? (
1156 class="btn btn-secondary"
1157 aria-label={purgeTypeText}
1169 let post = this.props.post_view.post;
1170 return post.thumbnail_url.isSome() ||
1171 post.url.map(isImage).unwrapOr(false) ? (
1173 <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1174 {this.postTitleLine()}
1177 {/* Post body prev or thumbnail */}
1178 {!this.state.imageExpanded && this.thumbnail()}
1182 this.postTitleLine()
1186 showMobilePreview() {
1187 let post = this.props.post_view.post;
1191 some: body => <div className="md-div mb-1 preview-lines">{body}</div>,
1200 {/* The mobile view*/}
1201 <div class="d-block d-sm-none">
1203 <div class="col-12">
1204 {this.createdLine()}
1206 {/* If it has a thumbnail, do a right aligned thumbnail */}
1207 {this.mobileThumbnail()}
1209 {/* Show a preview of the post body */}
1210 {this.showMobilePreview()}
1212 {this.commentsLine(true)}
1213 {this.userActionsLine()}
1214 {this.duplicatesLine()}
1215 {this.removeAndBanDialogs()}
1220 {/* The larger view*/}
1221 <div class="d-none d-sm-block">
1223 {!this.props.viewOnly && this.voteBar()}
1224 <div class="col-sm-2 pr-0">
1225 <div class="">{this.thumbnail()}</div>
1227 <div class="col-12 col-sm-9">
1229 <div className="col-12">
1230 {this.postTitleLine()}
1231 {this.createdLine()}
1232 {this.commentsLine()}
1233 {this.duplicatesLine()}
1234 {this.userActionsLine()}
1235 {this.removeAndBanDialogs()}
1245 private get myPost(): boolean {
1246 return UserService.Instance.myUserInfo.match({
1248 this.props.post_view.creator.id == mui.local_user_view.person.id,
1253 handlePostLike(i: PostListing, event: any) {
1254 event.preventDefault();
1255 if (UserService.Instance.myUserInfo.isNone()) {
1256 this.context.router.history.push(`/login`);
1259 let myVote = this.state.my_vote.unwrapOr(0);
1260 let newVote = myVote == 1 ? 0 : 1;
1265 } else if (myVote == -1) {
1266 i.state.downvotes--;
1274 i.state.my_vote = Some(newVote);
1276 let form = new CreatePostLike({
1277 post_id: i.props.post_view.post.id,
1279 auth: auth().unwrap(),
1282 WebSocketService.Instance.send(wsClient.likePost(form));
1283 i.setState(i.state);
1287 handlePostDisLike(i: PostListing, event: any) {
1288 event.preventDefault();
1289 if (UserService.Instance.myUserInfo.isNone()) {
1290 this.context.router.history.push(`/login`);
1293 let myVote = this.state.my_vote.unwrapOr(0);
1294 let newVote = myVote == -1 ? 0 : -1;
1299 i.state.downvotes++;
1300 } else if (myVote == -1) {
1301 i.state.downvotes--;
1304 i.state.downvotes++;
1308 i.state.my_vote = Some(newVote);
1310 let form = new CreatePostLike({
1311 post_id: i.props.post_view.post.id,
1313 auth: auth().unwrap(),
1316 WebSocketService.Instance.send(wsClient.likePost(form));
1317 i.setState(i.state);
1321 handleEditClick(i: PostListing) {
1322 i.state.showEdit = true;
1323 i.setState(i.state);
1326 handleEditCancel() {
1327 this.state.showEdit = false;
1328 this.setState(this.state);
1331 // The actual editing is done in the recieve for post
1333 this.state.showEdit = false;
1334 this.setState(this.state);
1337 handleShowReportDialog(i: PostListing) {
1338 i.state.showReportDialog = !i.state.showReportDialog;
1339 i.setState(this.state);
1342 handleReportReasonChange(i: PostListing, event: any) {
1343 i.state.reportReason = Some(event.target.value);
1344 i.setState(i.state);
1347 handleReportSubmit(i: PostListing, event: any) {
1348 event.preventDefault();
1349 let form = new CreatePostReport({
1350 post_id: i.props.post_view.post.id,
1351 reason: toUndefined(i.state.reportReason),
1352 auth: auth().unwrap(),
1354 WebSocketService.Instance.send(wsClient.createPostReport(form));
1356 i.state.showReportDialog = false;
1357 i.setState(i.state);
1360 handleBlockUserClick(i: PostListing) {
1361 let blockUserForm = new BlockPerson({
1362 person_id: i.props.post_view.creator.id,
1364 auth: auth().unwrap(),
1366 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1369 handleDeleteClick(i: PostListing) {
1370 let deleteForm = new DeletePost({
1371 post_id: i.props.post_view.post.id,
1372 deleted: !i.props.post_view.post.deleted,
1373 auth: auth().unwrap(),
1375 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1378 handleSavePostClick(i: PostListing) {
1380 i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1381 let form = new SavePost({
1382 post_id: i.props.post_view.post.id,
1384 auth: auth().unwrap(),
1387 WebSocketService.Instance.send(wsClient.savePost(form));
1390 get crossPostParams(): string {
1391 let post = this.props.post_view.post;
1392 let params = `?title=${encodeURIComponent(post.name)}`;
1394 if (post.url.isSome()) {
1395 params += `&url=${encodeURIComponent(post.url.unwrap())}`;
1397 if (post.body.isSome()) {
1398 params += `&body=${encodeURIComponent(this.crossPostBody())}`;
1403 crossPostBody(): string {
1404 let post = this.props.post_view.post;
1405 let body = `${i18n.t("cross_posted_from")} ${post.ap_id}\n\n${post.body
1407 .replace(/^/gm, "> ")}`;
1411 get showBody(): boolean {
1412 return this.props.showBody || this.state.showBody;
1415 handleModRemoveShow(i: PostListing) {
1416 i.state.showRemoveDialog = !i.state.showRemoveDialog;
1417 i.state.showBanDialog = false;
1418 i.setState(i.state);
1421 handleModRemoveReasonChange(i: PostListing, event: any) {
1422 i.state.removeReason = Some(event.target.value);
1423 i.setState(i.state);
1426 handleModRemoveDataChange(i: PostListing, event: any) {
1427 i.state.removeData = event.target.checked;
1428 i.setState(i.state);
1431 handleModRemoveSubmit(i: PostListing, event: any) {
1432 event.preventDefault();
1433 let form = new RemovePost({
1434 post_id: i.props.post_view.post.id,
1435 removed: !i.props.post_view.post.removed,
1436 reason: i.state.removeReason,
1437 auth: auth().unwrap(),
1439 WebSocketService.Instance.send(wsClient.removePost(form));
1441 i.state.showRemoveDialog = false;
1442 i.setState(i.state);
1445 handleModLock(i: PostListing) {
1446 let form = new LockPost({
1447 post_id: i.props.post_view.post.id,
1448 locked: !i.props.post_view.post.locked,
1449 auth: auth().unwrap(),
1451 WebSocketService.Instance.send(wsClient.lockPost(form));
1454 handleModSticky(i: PostListing) {
1455 let form = new StickyPost({
1456 post_id: i.props.post_view.post.id,
1457 stickied: !i.props.post_view.post.stickied,
1458 auth: auth().unwrap(),
1460 WebSocketService.Instance.send(wsClient.stickyPost(form));
1463 handleModBanFromCommunityShow(i: PostListing) {
1464 i.state.showBanDialog = true;
1465 i.state.banType = BanType.Community;
1466 i.state.showRemoveDialog = false;
1467 i.setState(i.state);
1470 handleModBanShow(i: PostListing) {
1471 i.state.showBanDialog = true;
1472 i.state.banType = BanType.Site;
1473 i.state.showRemoveDialog = false;
1474 i.setState(i.state);
1477 handlePurgePersonShow(i: PostListing) {
1478 i.state.showPurgeDialog = true;
1479 i.state.purgeType = PurgeType.Person;
1480 i.state.showRemoveDialog = false;
1481 i.setState(i.state);
1484 handlePurgePostShow(i: PostListing) {
1485 i.state.showPurgeDialog = true;
1486 i.state.purgeType = PurgeType.Post;
1487 i.state.showRemoveDialog = false;
1488 i.setState(i.state);
1491 handlePurgeReasonChange(i: PostListing, event: any) {
1492 i.state.purgeReason = Some(event.target.value);
1493 i.setState(i.state);
1496 handlePurgeSubmit(i: PostListing, event: any) {
1497 event.preventDefault();
1499 if (i.state.purgeType == PurgeType.Person) {
1500 let form = new PurgePerson({
1501 person_id: i.props.post_view.creator.id,
1502 reason: i.state.purgeReason,
1503 auth: auth().unwrap(),
1505 WebSocketService.Instance.send(wsClient.purgePerson(form));
1506 } else if (i.state.purgeType == PurgeType.Post) {
1507 let form = new PurgePost({
1508 post_id: i.props.post_view.post.id,
1509 reason: i.state.purgeReason,
1510 auth: auth().unwrap(),
1512 WebSocketService.Instance.send(wsClient.purgePost(form));
1515 i.state.purgeLoading = true;
1516 i.setState(i.state);
1519 handleModBanReasonChange(i: PostListing, event: any) {
1520 i.state.banReason = Some(event.target.value);
1521 i.setState(i.state);
1524 handleModBanExpireDaysChange(i: PostListing, event: any) {
1525 i.state.banExpireDays = Some(event.target.value);
1526 i.setState(i.state);
1529 handleModBanFromCommunitySubmit(i: PostListing) {
1530 i.state.banType = BanType.Community;
1531 i.setState(i.state);
1532 i.handleModBanBothSubmit(i);
1535 handleModBanSubmit(i: PostListing) {
1536 i.state.banType = BanType.Site;
1537 i.setState(i.state);
1538 i.handleModBanBothSubmit(i);
1541 handleModBanBothSubmit(i: PostListing, event?: any) {
1542 if (event) event.preventDefault();
1544 if (i.state.banType == BanType.Community) {
1545 // If its an unban, restore all their data
1546 let ban = !i.props.post_view.creator_banned_from_community;
1548 i.state.removeData = false;
1550 let form = new BanFromCommunity({
1551 person_id: i.props.post_view.creator.id,
1552 community_id: i.props.post_view.community.id,
1554 remove_data: Some(i.state.removeData),
1555 reason: i.state.banReason,
1556 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1557 auth: auth().unwrap(),
1559 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1561 // If its an unban, restore all their data
1562 let ban = !i.props.post_view.creator.banned;
1564 i.state.removeData = false;
1566 let form = new BanPerson({
1567 person_id: i.props.post_view.creator.id,
1569 remove_data: Some(i.state.removeData),
1570 reason: i.state.banReason,
1571 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1572 auth: auth().unwrap(),
1574 WebSocketService.Instance.send(wsClient.banPerson(form));
1577 i.state.showBanDialog = false;
1578 i.setState(i.state);
1581 handleAddModToCommunity(i: PostListing) {
1582 let form = new AddModToCommunity({
1583 person_id: i.props.post_view.creator.id,
1584 community_id: i.props.post_view.community.id,
1585 added: !i.creatorIsMod_,
1586 auth: auth().unwrap(),
1588 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1589 i.setState(i.state);
1592 handleAddAdmin(i: PostListing) {
1593 let form = new AddAdmin({
1594 person_id: i.props.post_view.creator.id,
1595 added: !i.creatorIsAdmin_,
1596 auth: auth().unwrap(),
1598 WebSocketService.Instance.send(wsClient.addAdmin(form));
1599 i.setState(i.state);
1602 handleShowConfirmTransferCommunity(i: PostListing) {
1603 i.state.showConfirmTransferCommunity = true;
1604 i.setState(i.state);
1607 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1608 i.state.showConfirmTransferCommunity = false;
1609 i.setState(i.state);
1612 handleTransferCommunity(i: PostListing) {
1613 let form = new TransferCommunity({
1614 community_id: i.props.post_view.community.id,
1615 person_id: i.props.post_view.creator.id,
1616 auth: auth().unwrap(),
1618 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1619 i.state.showConfirmTransferCommunity = false;
1620 i.setState(i.state);
1623 handleShowConfirmTransferSite(i: PostListing) {
1624 i.state.showConfirmTransferSite = true;
1625 i.setState(i.state);
1628 handleCancelShowConfirmTransferSite(i: PostListing) {
1629 i.state.showConfirmTransferSite = false;
1630 i.setState(i.state);
1633 handleImageExpandClick(i: PostListing, event: any) {
1634 event.preventDefault();
1635 i.state.imageExpanded = !i.state.imageExpanded;
1636 i.setState(i.state);
1640 handleViewSource(i: PostListing) {
1641 i.state.viewSource = !i.state.viewSource;
1642 i.setState(i.state);
1645 handleShowAdvanced(i: PostListing) {
1646 i.state.showAdvanced = !i.state.showAdvanced;
1647 i.setState(i.state);
1651 handleShowMoreMobile(i: PostListing) {
1652 i.state.showMoreMobile = !i.state.showMoreMobile;
1653 i.state.showAdvanced = !i.state.showAdvanced;
1654 i.setState(i.state);
1658 handleShowBody(i: PostListing) {
1659 i.state.showBody = !i.state.showBody;
1660 i.setState(i.state);
1664 get pointsTippy(): string {
1665 let points = i18n.t("number_of_points", {
1666 count: this.state.score,
1667 formattedCount: this.state.score,
1670 let upvotes = i18n.t("number_of_upvotes", {
1671 count: this.state.upvotes,
1672 formattedCount: this.state.upvotes,
1675 let downvotes = i18n.t("number_of_downvotes", {
1676 count: this.state.downvotes,
1677 formattedCount: this.state.downvotes,
1680 return `${points} • ${upvotes} • ${downvotes}`;
1683 get canModOnSelf_(): boolean {
1685 this.props.moderators,
1687 this.props.post_view.creator.id,
1693 get canMod_(): boolean {
1695 this.props.moderators,
1697 this.props.post_view.creator.id
1701 get canAdmin_(): boolean {
1702 return canAdmin(this.props.admins, this.props.post_view.creator.id);
1705 get creatorIsMod_(): boolean {
1706 return isMod(this.props.moderators, this.props.post_view.creator.id);
1709 get creatorIsAdmin_(): boolean {
1710 return isAdmin(this.props.admins, this.props.post_view.creator.id);