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,
23 } from "lemmy-js-client";
24 import { externalHost } from "../../env";
25 import { i18n } from "../../i18next";
26 import { BanType } from "../../interfaces";
27 import { UserService, WebSocketService } from "../../services";
48 import { Icon } from "../common/icon";
49 import { MomentTime } from "../common/moment-time";
50 import { PictrsImage } from "../common/pictrs-image";
51 import { CommunityLink } from "../community/community-link";
52 import { PersonListing } from "../person/person-listing";
53 import { MetadataCard } from "./metadata-card";
54 import { PostForm } from "./post-form";
56 interface PostListingState {
58 showRemoveDialog: boolean;
59 removeReason: Option<string>;
60 showBanDialog: boolean;
61 banReason: Option<string>;
62 banExpireDays: Option<number>;
65 showConfirmTransferSite: boolean;
66 showConfirmTransferCommunity: boolean;
67 imageExpanded: boolean;
69 showAdvanced: boolean;
70 showMoreMobile: boolean;
72 showReportDialog: boolean;
73 reportReason: Option<string>;
74 my_vote: Option<number>;
80 interface PostListingProps {
82 duplicates: Option<PostView[]>;
83 moderators: Option<CommunityModeratorView[]>;
84 admins: Option<PersonViewSafe[]>;
85 showCommunity?: boolean;
87 enableDownvotes?: boolean;
92 export class PostListing extends Component<PostListingProps, PostListingState> {
93 private emptyState: PostListingState = {
95 showRemoveDialog: false,
100 banType: BanType.Community,
102 showConfirmTransferSite: false,
103 showConfirmTransferCommunity: false,
104 imageExpanded: false,
107 showMoreMobile: false,
109 showReportDialog: false,
111 my_vote: this.props.post_view.my_vote,
112 score: this.props.post_view.counts.score,
113 upvotes: this.props.post_view.counts.upvotes,
114 downvotes: this.props.post_view.counts.downvotes,
117 constructor(props: any, context: any) {
118 super(props, context);
120 this.state = this.emptyState;
121 this.handlePostLike = this.handlePostLike.bind(this);
122 this.handlePostDisLike = this.handlePostDisLike.bind(this);
123 this.handleEditPost = this.handleEditPost.bind(this);
124 this.handleEditCancel = this.handleEditCancel.bind(this);
127 componentWillReceiveProps(nextProps: PostListingProps) {
128 this.state.my_vote = nextProps.post_view.my_vote;
129 this.state.upvotes = nextProps.post_view.counts.upvotes;
130 this.state.downvotes = nextProps.post_view.counts.downvotes;
131 this.state.score = nextProps.post_view.counts.score;
132 if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
133 this.state.imageExpanded = false;
135 this.setState(this.state);
139 let post = this.props.post_view.post;
141 <div class="post-listing">
142 {!this.state.showEdit ? (
145 {this.state.imageExpanded && this.img}
146 {post.url.isSome() &&
148 post.embed_title.isSome() && <MetadataCard post={post} />}
149 {this.showBody && this.body()}
154 post_view={Some(this.props.post_view)}
157 onEdit={this.handleEditPost}
158 onCancel={this.handleEditCancel}
159 enableNsfw={this.props.enableNsfw}
160 enableDownvotes={this.props.enableDownvotes}
169 return this.props.post_view.post.body.match({
171 <div class="col-12 card my-2 p-2">
172 {this.state.viewSource ? (
175 <div className="md-div" dangerouslySetInnerHTML={mdToHtml(body)} />
184 return this.imageSrc.match({
187 <div class="offset-sm-3 my-2 d-none d-sm-block">
188 <a href={src} class="d-inline-block">
189 <PictrsImage src={src} />
192 <div className="my-2 d-block d-sm-none">
194 class="d-inline-block"
195 onClick={linkEvent(this, this.handleImageExpandClick)}
197 <PictrsImage src={src} />
206 imgThumb(src: string) {
207 let post_view = this.props.post_view;
213 nsfw={post_view.post.nsfw || post_view.community.nsfw}
218 get imageSrc(): Option<string> {
219 let post = this.props.post_view.post;
221 let thumbnail = post.thumbnail_url;
223 if (url.isSome() && isImage(url.unwrap())) {
224 if (url.unwrap().includes("pictrs")) {
226 } else if (thumbnail.isSome()) {
231 } else if (thumbnail.isSome()) {
239 let post = this.props.post_view.post;
241 let thumbnail = post.thumbnail_url;
243 if (url.isSome() && isImage(url.unwrap())) {
246 href={this.imageSrc.unwrap()}
247 class="text-body d-inline-block position-relative mb-2"
248 data-tippy-content={i18n.t("expand_here")}
249 onClick={linkEvent(this, this.handleImageExpandClick)}
250 aria-label={i18n.t("expand_here")}
252 {this.imgThumb(this.imageSrc.unwrap())}
253 <Icon icon="image" classes="mini-overlay" />
256 } else if (url.isSome() && thumbnail.isSome()) {
259 class="text-body d-inline-block position-relative mb-2"
264 {this.imgThumb(this.imageSrc.unwrap())}
265 <Icon icon="external-link" classes="mini-overlay" />
268 } else if (url.isSome()) {
269 if (isVideo(url.unwrap())) {
271 <div class="embed-responsive embed-responsive-16by9">
277 class="embed-responsive-item"
279 <source src={url.unwrap()} type="video/mp4" />
286 className="text-body"
291 <div class="thumbnail rounded bg-light d-flex justify-content-center">
292 <Icon icon="external-link" classes="d-flex align-items-center" />
300 className="text-body"
301 to={`/post/${post.id}`}
302 title={i18n.t("comments")}
304 <div class="thumbnail rounded bg-light d-flex justify-content-center">
305 <Icon icon="message-square" classes="d-flex align-items-center" />
313 let post_view = this.props.post_view;
315 <ul class="list-inline mb-1 text-muted small">
316 <li className="list-inline-item">
317 <PersonListing person={post_view.creator} />
319 {this.creatorIsMod_ && (
320 <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
322 {this.creatorIsAdmin_ && (
323 <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
325 {post_view.creator.bot_account && (
326 <span className="mx-1 badge badge-light">
327 {i18n.t("bot_account").toLowerCase()}
330 {(post_view.creator_banned_from_community ||
331 isBanned(post_view.creator)) && (
332 <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
334 {post_view.creator_blocked && (
335 <span className="mx-1 badge badge-danger">{"blocked"}</span>
337 {this.props.showCommunity && (
339 <span class="mx-1"> {i18n.t("to")} </span>
340 <CommunityLink community={post_view.community} />
344 <li className="list-inline-item">•</li>
345 {post_view.post.url.match({
347 !(hostname(url) == externalHost) && (
349 <li className="list-inline-item">
351 className="text-muted font-italic"
359 <li className="list-inline-item">•</li>
364 <li className="list-inline-item">
367 published={post_view.post.published}
368 updated={post_view.post.updated}
372 {post_view.post.body.match({
375 <li className="list-inline-item">•</li>
376 <li className="list-inline-item">
378 className="text-muted btn btn-sm btn-link p-0"
379 data-tippy-content={md.render(body)}
380 data-tippy-allowHtml={true}
381 onClick={linkEvent(this, this.handleShowBody)}
383 <Icon icon="book-open" classes="icon-inline mr-1" />
396 <div className={`vote-bar col-1 pr-0 small text-center`}>
398 className={`btn-animate btn btn-link p-0 ${
399 this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted"
401 onClick={linkEvent(this, this.handlePostLike)}
402 data-tippy-content={i18n.t("upvote")}
403 aria-label={i18n.t("upvote")}
405 <Icon icon="arrow-up1" classes="upvote" />
409 class={`unselectable pointer font-weight-bold text-muted px-1`}
410 data-tippy-content={this.pointsTippy}
412 {numToSI(this.state.score)}
415 <div class="p-1"></div>
417 {this.props.enableDownvotes && (
419 className={`btn-animate btn btn-link p-0 ${
420 this.state.my_vote.unwrapOr(0) == -1
424 onClick={linkEvent(this, this.handlePostDisLike)}
425 data-tippy-content={i18n.t("downvote")}
426 aria-label={i18n.t("downvote")}
428 <Icon icon="arrow-down1" classes="downvote" />
436 let post = this.props.post_view.post;
438 <div className="post-title overflow-hidden">
443 className={!post.stickied ? "text-body" : "text-primary"}
453 className={!post.stickied ? "text-body" : "text-primary"}
454 to={`/post/${post.id}`}
455 title={i18n.t("comments")}
461 {post.url.map(isImage).or(post.thumbnail_url).unwrapOr(false) && (
463 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
464 data-tippy-content={i18n.t("expand_here")}
465 onClick={linkEvent(this, this.handleImageExpandClick)}
469 !this.state.imageExpanded ? "plus-square" : "minus-square"
471 classes="icon-inline"
476 <small className="ml-2 text-muted font-italic">
482 className="unselectable pointer ml-2 text-muted font-italic"
483 data-tippy-content={i18n.t("deleted")}
485 <Icon icon="trash" classes="icon-inline text-danger" />
490 className="unselectable pointer ml-2 text-muted font-italic"
491 data-tippy-content={i18n.t("locked")}
493 <Icon icon="lock" classes="icon-inline text-danger" />
498 className="unselectable pointer ml-2 text-muted font-italic"
499 data-tippy-content={i18n.t("stickied")}
501 <Icon icon="pin" classes="icon-inline text-primary" />
505 <small className="ml-2 text-muted font-italic">
515 return this.props.duplicates.match({
517 dupes.length > 0 && (
518 <ul class="list-inline mb-1 small text-muted">
520 <li className="list-inline-item mr-2">
521 {i18n.t("cross_posted_to")}
524 <li className="list-inline-item mr-2">
525 <Link to={`/post/${pv.post.id}`}>
528 : `${pv.community.name}@${hostname(
529 pv.community.actor_id
541 commentsLine(mobile = false) {
542 let post = this.props.post_view.post;
544 <div class="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1">
545 {this.commentsButton}
548 className="btn btn-link btn-animate text-muted py-0"
549 title={i18n.t("link")}
552 <Icon icon="fedilink" inline />
555 {mobile && !this.props.viewOnly && this.mobileVotes}
556 {UserService.Instance.myUserInfo.isSome() &&
557 !this.props.viewOnly &&
558 this.postActions(mobile)}
563 postActions(mobile = false) {
564 // Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
565 // Possible enhancement: Make each button a component.
566 let post_view = this.props.post_view;
570 {this.crossPostButton}
571 {mobile && this.showMoreButton}
572 {(!mobile || this.state.showAdvanced) && (
580 {this.myPost && (this.showBody || this.state.showAdvanced) && (
588 {this.state.showAdvanced && (
591 post_view.post.body.isSome() &&
592 this.viewSourceButton}
593 {this.canModOnSelf_ && (
599 {(this.canMod_ || this.canAdmin_) && <>{this.modRemoveButton}</>}
602 {!mobile && this.showMoreButton}
607 get commentsButton() {
608 let post_view = this.props.post_view;
610 <button class="btn btn-link text-muted py-0 pl-0">
612 className="text-muted"
613 title={i18n.t("number_of_comments", {
614 count: post_view.counts.comments,
615 formattedCount: post_view.counts.comments,
617 to={`/post/${post_view.post.id}?scrollToComments=true`}
619 <Icon icon="message-square" classes="mr-1" inline />
620 {i18n.t("number_of_comments", {
621 count: post_view.counts.comments,
622 formattedCount: numToSI(post_view.counts.comments),
631 let tippy = showScores() ? { "data-tippy-content": this.pointsTippy } : {};
636 className={`btn-animate btn py-0 px-1 ${
637 this.state.my_vote.unwrapOr(0) == 1 ? "text-info" : "text-muted"
640 onClick={linkEvent(this, this.handlePostLike)}
641 aria-label={i18n.t("upvote")}
643 <Icon icon="arrow-up1" classes="icon-inline small" />
645 <span class="ml-2">{numToSI(this.state.upvotes)}</span>
648 {this.props.enableDownvotes && (
650 className={`ml-2 btn-animate btn py-0 px-1 ${
651 this.state.my_vote.unwrapOr(0) == -1
655 onClick={linkEvent(this, this.handlePostDisLike)}
657 aria-label={i18n.t("downvote")}
659 <Icon icon="arrow-down1" classes="icon-inline small" />
662 class={classNames("ml-2", {
663 invisible: this.state.downvotes === 0,
666 {numToSI(this.state.downvotes)}
677 let saved = this.props.post_view.saved;
678 let label = saved ? i18n.t("unsave") : i18n.t("save");
681 class="btn btn-link btn-animate text-muted py-0"
682 onClick={linkEvent(this, this.handleSavePostClick)}
683 data-tippy-content={label}
688 classes={classNames({ "text-warning": saved })}
695 get crossPostButton() {
698 className="btn btn-link btn-animate text-muted py-0"
699 to={`/create_post${this.crossPostParams}`}
700 title={i18n.t("cross_post")}
702 <Icon icon="copy" inline />
710 class="btn btn-link btn-animate text-muted py-0"
711 onClick={linkEvent(this, this.handleShowReportDialog)}
712 data-tippy-content={i18n.t("show_report_dialog")}
713 aria-label={i18n.t("show_report_dialog")}
715 <Icon icon="flag" inline />
723 class="btn btn-link btn-animate text-muted py-0"
724 onClick={linkEvent(this, this.handleBlockUserClick)}
725 data-tippy-content={i18n.t("block_user")}
726 aria-label={i18n.t("block_user")}
728 <Icon icon="slash" inline />
736 class="btn btn-link btn-animate text-muted py-0"
737 onClick={linkEvent(this, this.handleEditClick)}
738 data-tippy-content={i18n.t("edit")}
739 aria-label={i18n.t("edit")}
741 <Icon icon="edit" inline />
747 let deleted = this.props.post_view.post.deleted;
748 let label = !deleted ? i18n.t("delete") : i18n.t("restore");
751 class="btn btn-link btn-animate text-muted py-0"
752 onClick={linkEvent(this, this.handleDeleteClick)}
753 data-tippy-content={label}
758 classes={classNames({ "text-danger": deleted })}
765 get showMoreButton() {
768 class="btn btn-link btn-animate text-muted py-0"
769 onClick={linkEvent(this, this.handleShowAdvanced)}
770 data-tippy-content={i18n.t("more")}
771 aria-label={i18n.t("more")}
773 <Icon icon="more-vertical" inline />
778 get viewSourceButton() {
781 class="btn btn-link btn-animate text-muted py-0"
782 onClick={linkEvent(this, this.handleViewSource)}
783 data-tippy-content={i18n.t("view_source")}
784 aria-label={i18n.t("view_source")}
788 classes={classNames({ "text-success": this.state.viewSource })}
796 let locked = this.props.post_view.post.locked;
797 let label = locked ? i18n.t("unlock") : i18n.t("lock");
800 class="btn btn-link btn-animate text-muted py-0"
801 onClick={linkEvent(this, this.handleModLock)}
802 data-tippy-content={label}
807 classes={classNames({ "text-danger": locked })}
815 let stickied = this.props.post_view.post.stickied;
816 let label = stickied ? i18n.t("unsticky") : i18n.t("sticky");
819 class="btn btn-link btn-animate text-muted py-0"
820 onClick={linkEvent(this, this.handleModSticky)}
821 data-tippy-content={label}
826 classes={classNames({ "text-success": stickied })}
833 get modRemoveButton() {
834 let removed = this.props.post_view.post.removed;
837 class="btn btn-link btn-animate text-muted py-0"
840 !removed ? this.handleModRemoveShow : this.handleModRemoveSubmit
843 {/* TODO: Find an icon for this. */}
844 {!removed ? i18n.t("remove") : i18n.t("restore")}
850 * Mod/Admin actions to be taken against the author.
854 let post_view = this.props.post_view;
856 this.state.showAdvanced && (
860 {!this.creatorIsMod_ &&
861 (!post_view.creator_banned_from_community ? (
863 class="btn btn-link btn-animate text-muted py-0"
866 this.handleModBanFromCommunityShow
868 aria-label={i18n.t("ban")}
874 class="btn btn-link btn-animate text-muted py-0"
877 this.handleModBanFromCommunitySubmit
879 aria-label={i18n.t("unban")}
884 {!post_view.creator_banned_from_community && (
886 class="btn btn-link btn-animate text-muted py-0"
887 onClick={linkEvent(this, this.handleAddModToCommunity)}
890 ? i18n.t("remove_as_mod")
891 : i18n.t("appoint_as_mod")
895 ? i18n.t("remove_as_mod")
896 : i18n.t("appoint_as_mod")}
901 {/* Community creators and admins can transfer community to another mod */}
902 {(amCommunityCreator(this.props.moderators, post_view.creator.id) ||
904 this.creatorIsMod_ &&
905 (!this.state.showConfirmTransferCommunity ? (
907 class="btn btn-link btn-animate text-muted py-0"
910 this.handleShowConfirmTransferCommunity
912 aria-label={i18n.t("transfer_community")}
914 {i18n.t("transfer_community")}
919 class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
920 aria-label={i18n.t("are_you_sure")}
922 {i18n.t("are_you_sure")}
925 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
926 aria-label={i18n.t("yes")}
927 onClick={linkEvent(this, this.handleTransferCommunity)}
932 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
935 this.handleCancelShowConfirmTransferCommunity
937 aria-label={i18n.t("no")}
943 {/* Admins can ban from all, and appoint other admins */}
946 {!this.creatorIsAdmin_ &&
947 (!isBanned(post_view.creator) ? (
949 class="btn btn-link btn-animate text-muted py-0"
950 onClick={linkEvent(this, this.handleModBanShow)}
951 aria-label={i18n.t("ban_from_site")}
953 {i18n.t("ban_from_site")}
957 class="btn btn-link btn-animate text-muted py-0"
958 onClick={linkEvent(this, this.handleModBanSubmit)}
959 aria-label={i18n.t("unban_from_site")}
961 {i18n.t("unban_from_site")}
964 {!isBanned(post_view.creator) && post_view.creator.local && (
966 class="btn btn-link btn-animate text-muted py-0"
967 onClick={linkEvent(this, this.handleAddAdmin)}
970 ? i18n.t("remove_as_admin")
971 : i18n.t("appoint_as_admin")
974 {this.creatorIsAdmin_
975 ? i18n.t("remove_as_admin")
976 : i18n.t("appoint_as_admin")}
986 removeAndBanDialogs() {
987 let post = this.props.post_view;
990 {this.state.showRemoveDialog && (
993 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
995 <label class="sr-only" htmlFor="post-listing-remove-reason">
1000 id="post-listing-remove-reason"
1001 class="form-control mr-2"
1002 placeholder={i18n.t("reason")}
1003 value={toUndefined(this.state.removeReason)}
1004 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1008 class="btn btn-secondary"
1009 aria-label={i18n.t("remove_post")}
1011 {i18n.t("remove_post")}
1015 {this.state.showBanDialog && (
1016 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1017 <div class="form-group row col-12">
1018 <label class="col-form-label" htmlFor="post-listing-ban-reason">
1023 id="post-listing-ban-reason"
1024 class="form-control mr-2"
1025 placeholder={i18n.t("reason")}
1026 value={toUndefined(this.state.banReason)}
1027 onInput={linkEvent(this, this.handleModBanReasonChange)}
1029 <label class="col-form-label" htmlFor={`mod-ban-expires`}>
1034 id={`mod-ban-expires`}
1035 class="form-control mr-2"
1036 placeholder={i18n.t("number_of_days")}
1037 value={toUndefined(this.state.banExpireDays)}
1038 onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
1040 <div class="form-group">
1041 <div class="form-check">
1043 class="form-check-input"
1044 id="mod-ban-remove-data"
1046 checked={this.state.removeData}
1047 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1050 class="form-check-label"
1051 htmlFor="mod-ban-remove-data"
1052 title={i18n.t("remove_content_more")}
1054 {i18n.t("remove_content")}
1059 {/* TODO hold off on expires until later */}
1060 {/* <div class="form-group row"> */}
1061 {/* <label class="col-form-label">Expires</label> */}
1062 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1064 <div class="form-group row">
1067 class="btn btn-secondary"
1068 aria-label={i18n.t("ban")}
1070 {i18n.t("ban")} {post.creator.name}
1075 {this.state.showReportDialog && (
1078 onSubmit={linkEvent(this, this.handleReportSubmit)}
1080 <label class="sr-only" htmlFor="post-report-reason">
1085 id="post-report-reason"
1086 class="form-control mr-2"
1087 placeholder={i18n.t("reason")}
1089 value={toUndefined(this.state.reportReason)}
1090 onInput={linkEvent(this, this.handleReportReasonChange)}
1094 class="btn btn-secondary"
1095 aria-label={i18n.t("create_report")}
1097 {i18n.t("create_report")}
1106 let post = this.props.post_view.post;
1107 return post.thumbnail_url.isSome() ||
1108 post.url.map(isImage).unwrapOr(false) ? (
1110 <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1111 {this.postTitleLine()}
1114 {/* Post body prev or thumbnail */}
1115 {!this.state.imageExpanded && this.thumbnail()}
1119 this.postTitleLine()
1123 showMobilePreview() {
1124 let post = this.props.post_view.post;
1128 some: body => <div className="md-div mb-1 preview-lines">{body}</div>,
1137 {/* The mobile view*/}
1138 <div class="d-block d-sm-none">
1140 <div class="col-12">
1141 {this.createdLine()}
1143 {/* If it has a thumbnail, do a right aligned thumbnail */}
1144 {this.mobileThumbnail()}
1146 {/* Show a preview of the post body */}
1147 {this.showMobilePreview()}
1149 {this.commentsLine(true)}
1150 {this.userActionsLine()}
1151 {this.duplicatesLine()}
1152 {this.removeAndBanDialogs()}
1157 {/* The larger view*/}
1158 <div class="d-none d-sm-block">
1160 {!this.props.viewOnly && this.voteBar()}
1161 <div class="col-sm-2 pr-0">
1162 <div class="">{this.thumbnail()}</div>
1164 <div class="col-12 col-sm-9">
1166 <div className="col-12">
1167 {this.postTitleLine()}
1168 {this.createdLine()}
1169 {this.commentsLine()}
1170 {this.duplicatesLine()}
1171 {this.userActionsLine()}
1172 {this.removeAndBanDialogs()}
1182 private get myPost(): boolean {
1183 return UserService.Instance.myUserInfo.match({
1185 this.props.post_view.creator.id == mui.local_user_view.person.id,
1190 handlePostLike(i: PostListing, event: any) {
1191 event.preventDefault();
1192 if (UserService.Instance.myUserInfo.isNone()) {
1193 this.context.router.history.push(`/login`);
1196 let myVote = this.state.my_vote.unwrapOr(0);
1197 let newVote = myVote == 1 ? 0 : 1;
1202 } else if (myVote == -1) {
1203 i.state.downvotes--;
1211 i.state.my_vote = Some(newVote);
1213 let form = new CreatePostLike({
1214 post_id: i.props.post_view.post.id,
1216 auth: auth().unwrap(),
1219 WebSocketService.Instance.send(wsClient.likePost(form));
1220 i.setState(i.state);
1224 handlePostDisLike(i: PostListing, event: any) {
1225 event.preventDefault();
1226 if (UserService.Instance.myUserInfo.isNone()) {
1227 this.context.router.history.push(`/login`);
1230 let myVote = this.state.my_vote.unwrapOr(0);
1231 let newVote = myVote == -1 ? 0 : -1;
1236 i.state.downvotes++;
1237 } else if (myVote == -1) {
1238 i.state.downvotes--;
1241 i.state.downvotes++;
1245 i.state.my_vote = Some(newVote);
1247 let form = new CreatePostLike({
1248 post_id: i.props.post_view.post.id,
1250 auth: auth().unwrap(),
1253 WebSocketService.Instance.send(wsClient.likePost(form));
1254 i.setState(i.state);
1258 handleEditClick(i: PostListing) {
1259 i.state.showEdit = true;
1260 i.setState(i.state);
1263 handleEditCancel() {
1264 this.state.showEdit = false;
1265 this.setState(this.state);
1268 // The actual editing is done in the recieve for post
1270 this.state.showEdit = false;
1271 this.setState(this.state);
1274 handleShowReportDialog(i: PostListing) {
1275 i.state.showReportDialog = !i.state.showReportDialog;
1276 i.setState(this.state);
1279 handleReportReasonChange(i: PostListing, event: any) {
1280 i.state.reportReason = Some(event.target.value);
1281 i.setState(i.state);
1284 handleReportSubmit(i: PostListing, event: any) {
1285 event.preventDefault();
1286 let form = new CreatePostReport({
1287 post_id: i.props.post_view.post.id,
1288 reason: toUndefined(i.state.reportReason),
1289 auth: auth().unwrap(),
1291 WebSocketService.Instance.send(wsClient.createPostReport(form));
1293 i.state.showReportDialog = false;
1294 i.setState(i.state);
1297 handleBlockUserClick(i: PostListing) {
1298 let blockUserForm = new BlockPerson({
1299 person_id: i.props.post_view.creator.id,
1301 auth: auth().unwrap(),
1303 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1306 handleDeleteClick(i: PostListing) {
1307 let deleteForm = new DeletePost({
1308 post_id: i.props.post_view.post.id,
1309 deleted: !i.props.post_view.post.deleted,
1310 auth: auth().unwrap(),
1312 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1315 handleSavePostClick(i: PostListing) {
1317 i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1318 let form = new SavePost({
1319 post_id: i.props.post_view.post.id,
1321 auth: auth().unwrap(),
1324 WebSocketService.Instance.send(wsClient.savePost(form));
1327 get crossPostParams(): string {
1328 let post = this.props.post_view.post;
1329 let params = `?title=${encodeURIComponent(post.name)}`;
1331 if (post.url.isSome()) {
1332 params += `&url=${encodeURIComponent(post.url.unwrap())}`;
1334 if (post.body.isSome()) {
1335 params += `&body=${encodeURIComponent(this.crossPostBody())}`;
1340 crossPostBody(): string {
1341 let post = this.props.post_view.post;
1342 let body = `${i18n.t("cross_posted_from")} ${post.ap_id}\n\n${post.body
1344 .replace(/^/gm, "> ")}`;
1348 get showBody(): boolean {
1349 return this.props.showBody || this.state.showBody;
1352 handleModRemoveShow(i: PostListing) {
1353 i.state.showRemoveDialog = !i.state.showRemoveDialog;
1354 i.state.showBanDialog = false;
1355 i.setState(i.state);
1358 handleModRemoveReasonChange(i: PostListing, event: any) {
1359 i.state.removeReason = Some(event.target.value);
1360 i.setState(i.state);
1363 handleModRemoveDataChange(i: PostListing, event: any) {
1364 i.state.removeData = event.target.checked;
1365 i.setState(i.state);
1368 handleModRemoveSubmit(i: PostListing, event: any) {
1369 event.preventDefault();
1370 let form = new RemovePost({
1371 post_id: i.props.post_view.post.id,
1372 removed: !i.props.post_view.post.removed,
1373 reason: i.state.removeReason,
1374 auth: auth().unwrap(),
1376 WebSocketService.Instance.send(wsClient.removePost(form));
1378 i.state.showRemoveDialog = false;
1379 i.setState(i.state);
1382 handleModLock(i: PostListing) {
1383 let form = new LockPost({
1384 post_id: i.props.post_view.post.id,
1385 locked: !i.props.post_view.post.locked,
1386 auth: auth().unwrap(),
1388 WebSocketService.Instance.send(wsClient.lockPost(form));
1391 handleModSticky(i: PostListing) {
1392 let form = new StickyPost({
1393 post_id: i.props.post_view.post.id,
1394 stickied: !i.props.post_view.post.stickied,
1395 auth: auth().unwrap(),
1397 WebSocketService.Instance.send(wsClient.stickyPost(form));
1400 handleModBanFromCommunityShow(i: PostListing) {
1401 i.state.showBanDialog = true;
1402 i.state.banType = BanType.Community;
1403 i.state.showRemoveDialog = false;
1404 i.setState(i.state);
1407 handleModBanShow(i: PostListing) {
1408 i.state.showBanDialog = true;
1409 i.state.banType = BanType.Site;
1410 i.state.showRemoveDialog = false;
1411 i.setState(i.state);
1414 handleModBanReasonChange(i: PostListing, event: any) {
1415 i.state.banReason = Some(event.target.value);
1416 i.setState(i.state);
1419 handleModBanExpireDaysChange(i: PostListing, event: any) {
1420 i.state.banExpireDays = Some(event.target.value);
1421 i.setState(i.state);
1424 handleModBanFromCommunitySubmit(i: PostListing) {
1425 i.state.banType = BanType.Community;
1426 i.setState(i.state);
1427 i.handleModBanBothSubmit(i);
1430 handleModBanSubmit(i: PostListing) {
1431 i.state.banType = BanType.Site;
1432 i.setState(i.state);
1433 i.handleModBanBothSubmit(i);
1436 handleModBanBothSubmit(i: PostListing, event?: any) {
1437 if (event) event.preventDefault();
1439 if (i.state.banType == BanType.Community) {
1440 // If its an unban, restore all their data
1441 let ban = !i.props.post_view.creator_banned_from_community;
1443 i.state.removeData = false;
1445 let form = new BanFromCommunity({
1446 person_id: i.props.post_view.creator.id,
1447 community_id: i.props.post_view.community.id,
1449 remove_data: Some(i.state.removeData),
1450 reason: i.state.banReason,
1451 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1452 auth: auth().unwrap(),
1454 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1456 // If its an unban, restore all their data
1457 let ban = !i.props.post_view.creator.banned;
1459 i.state.removeData = false;
1461 let form = new BanPerson({
1462 person_id: i.props.post_view.creator.id,
1464 remove_data: Some(i.state.removeData),
1465 reason: i.state.banReason,
1466 expires: i.state.banExpireDays.map(futureDaysToUnixTime),
1467 auth: auth().unwrap(),
1469 WebSocketService.Instance.send(wsClient.banPerson(form));
1472 i.state.showBanDialog = false;
1473 i.setState(i.state);
1476 handleAddModToCommunity(i: PostListing) {
1477 let form = new AddModToCommunity({
1478 person_id: i.props.post_view.creator.id,
1479 community_id: i.props.post_view.community.id,
1480 added: !i.creatorIsMod_,
1481 auth: auth().unwrap(),
1483 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1484 i.setState(i.state);
1487 handleAddAdmin(i: PostListing) {
1488 let form = new AddAdmin({
1489 person_id: i.props.post_view.creator.id,
1490 added: !i.creatorIsAdmin_,
1491 auth: auth().unwrap(),
1493 WebSocketService.Instance.send(wsClient.addAdmin(form));
1494 i.setState(i.state);
1497 handleShowConfirmTransferCommunity(i: PostListing) {
1498 i.state.showConfirmTransferCommunity = true;
1499 i.setState(i.state);
1502 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1503 i.state.showConfirmTransferCommunity = false;
1504 i.setState(i.state);
1507 handleTransferCommunity(i: PostListing) {
1508 let form = new TransferCommunity({
1509 community_id: i.props.post_view.community.id,
1510 person_id: i.props.post_view.creator.id,
1511 auth: auth().unwrap(),
1513 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1514 i.state.showConfirmTransferCommunity = false;
1515 i.setState(i.state);
1518 handleShowConfirmTransferSite(i: PostListing) {
1519 i.state.showConfirmTransferSite = true;
1520 i.setState(i.state);
1523 handleCancelShowConfirmTransferSite(i: PostListing) {
1524 i.state.showConfirmTransferSite = false;
1525 i.setState(i.state);
1528 handleImageExpandClick(i: PostListing, event: any) {
1529 event.preventDefault();
1530 i.state.imageExpanded = !i.state.imageExpanded;
1531 i.setState(i.state);
1535 handleViewSource(i: PostListing) {
1536 i.state.viewSource = !i.state.viewSource;
1537 i.setState(i.state);
1540 handleShowAdvanced(i: PostListing) {
1541 i.state.showAdvanced = !i.state.showAdvanced;
1542 i.setState(i.state);
1546 handleShowMoreMobile(i: PostListing) {
1547 i.state.showMoreMobile = !i.state.showMoreMobile;
1548 i.state.showAdvanced = !i.state.showAdvanced;
1549 i.setState(i.state);
1553 handleShowBody(i: PostListing) {
1554 i.state.showBody = !i.state.showBody;
1555 i.setState(i.state);
1559 get pointsTippy(): string {
1560 let points = i18n.t("number_of_points", {
1561 count: this.state.score,
1562 formattedCount: this.state.score,
1565 let upvotes = i18n.t("number_of_upvotes", {
1566 count: this.state.upvotes,
1567 formattedCount: this.state.upvotes,
1570 let downvotes = i18n.t("number_of_downvotes", {
1571 count: this.state.downvotes,
1572 formattedCount: this.state.downvotes,
1575 return `${points} • ${upvotes} • ${downvotes}`;
1578 get canModOnSelf_(): boolean {
1580 this.props.moderators,
1582 this.props.post_view.creator.id,
1588 get canMod_(): boolean {
1590 this.props.moderators,
1592 this.props.post_view.creator.id
1596 get canAdmin_(): boolean {
1597 return canAdmin(this.props.admins, this.props.post_view.creator.id);
1600 get creatorIsMod_(): boolean {
1601 return isMod(this.props.moderators, this.props.post_view.creator.id);
1604 get creatorIsAdmin_(): boolean {
1605 return isAdmin(this.props.admins, this.props.post_view.creator.id);