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_banned_from_community ||
292 post_view.creator.banned) && (
293 <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
295 {post_view.creator_blocked && (
296 <span className="mx-1 badge badge-danger">{"blocked"}</span>
298 {this.props.showCommunity && (
300 <span class="mx-1"> {i18n.t("to")} </span>
301 <CommunityLink community={post_view.community} />
305 <li className="list-inline-item">•</li>
306 {post_view.post.url && !(hostname(post_view.post.url) == externalHost) && (
308 <li className="list-inline-item">
310 className="text-muted font-italic"
311 href={post_view.post.url}
312 title={post_view.post.url}
315 {hostname(post_view.post.url)}
318 <li className="list-inline-item">•</li>
321 <li className="list-inline-item">
323 <MomentTime data={post_view.post} />
326 {post_view.post.body && (
328 <li className="list-inline-item">•</li>
329 <li className="list-inline-item">
331 className="text-muted btn btn-sm btn-link p-0"
332 data-tippy-content={md.render(
333 previewLines(post_view.post.body)
335 data-tippy-allowHtml={true}
336 onClick={linkEvent(this, this.handleShowBody)}
338 <Icon icon="book-open" classes="icon-inline mr-1" />
349 <div className={`vote-bar col-1 pr-0 small text-center`}>
351 className={`btn-animate btn btn-link p-0 ${
352 this.state.my_vote == 1 ? "text-info" : "text-muted"
354 onClick={linkEvent(this, this.handlePostLike)}
355 data-tippy-content={i18n.t("upvote")}
356 aria-label={i18n.t("upvote")}
358 <Icon icon="arrow-up1" classes="upvote" />
362 class={`unselectable pointer font-weight-bold text-muted px-1`}
363 data-tippy-content={this.pointsTippy}
365 {numToSI(this.state.score)}
368 <div class="p-1"></div>
370 {this.props.enableDownvotes && (
372 className={`btn-animate btn btn-link p-0 ${
373 this.state.my_vote == -1 ? "text-danger" : "text-muted"
375 onClick={linkEvent(this, this.handlePostDisLike)}
376 data-tippy-content={i18n.t("downvote")}
377 aria-label={i18n.t("downvote")}
379 <Icon icon="arrow-down1" classes="downvote" />
387 let post = this.props.post_view.post;
389 <div className="post-title overflow-hidden">
391 {this.showBody && post.url ? (
393 className={!post.stickied ? "text-body" : "text-primary"}
402 className={!post.stickied ? "text-body" : "text-primary"}
403 to={`/post/${post.id}`}
404 title={i18n.t("comments")}
409 {(isImage(post.url) || post.thumbnail_url) &&
410 (!this.state.imageExpanded ? (
412 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
413 data-tippy-content={i18n.t("expand_here")}
414 onClick={linkEvent(this, this.handleImageExpandClick)}
416 <Icon icon="plus-square" classes="icon-inline" />
421 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
422 onClick={linkEvent(this, this.handleImageExpandClick)}
424 <Icon icon="minus-square" classes="icon-inline" />
428 href={this.getImageSrc()}
429 class="btn btn-link d-inline-block"
430 onClick={linkEvent(this, this.handleImageExpandClick)}
432 <PictrsImage src={this.getImageSrc()} />
438 <small className="ml-2 text-muted font-italic">
444 className="unselectable pointer ml-2 text-muted font-italic"
445 data-tippy-content={i18n.t("deleted")}
447 <Icon icon="trash" classes="icon-inline text-danger" />
452 className="unselectable pointer ml-2 text-muted font-italic"
453 data-tippy-content={i18n.t("locked")}
455 <Icon icon="lock" classes="icon-inline text-danger" />
460 className="unselectable pointer ml-2 text-muted font-italic"
461 data-tippy-content={i18n.t("stickied")}
463 <Icon icon="pin" classes="icon-inline text-primary" />
467 <small className="ml-2 text-muted font-italic">
476 commentsLine(mobile = false) {
477 let post_view = this.props.post_view;
479 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
480 <button class="btn btn-link text-muted p-0">
482 className="text-muted small"
483 title={i18n.t("number_of_comments", {
484 count: post_view.counts.comments,
485 formattedCount: post_view.counts.comments,
487 to={`/post/${post_view.post.id}?scrollToComments=true`}
489 <Icon icon="message-square" classes="icon-inline mr-1" />
490 {i18n.t("number_of_comments", {
491 count: post_view.counts.comments,
492 formattedCount: numToSI(post_view.counts.comments),
498 {this.state.downvotes !== 0 && showScores() && (
500 class="btn text-muted py-0 pr-0"
501 data-tippy-content={this.pointsTippy}
502 aria-label={i18n.t("downvote")}
505 <Icon icon="arrow-down1" classes="icon-inline mr-1" />
506 <span>{numToSI(this.state.downvotes)}</span>
512 class="btn btn-link btn-animate text-muted py-0"
513 onClick={linkEvent(this, this.handleSavePostClick)}
515 post_view.saved ? i18n.t("unsave") : i18n.t("save")
517 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
522 classes={`icon-inline ${post_view.saved && "text-warning"}`}
529 {/* This is an expanding spacer for mobile */}
530 <div className="flex-grow-1"></div>
536 className={`btn-animate btn py-0 px-1 ${
537 this.state.my_vote == 1 ? "text-info" : "text-muted"
539 data-tippy-content={this.pointsTippy}
540 onClick={linkEvent(this, this.handlePostLike)}
541 aria-label={i18n.t("upvote")}
543 <Icon icon="arrow-up1" classes="icon-inline small mr-2" />
544 {numToSI(this.state.upvotes)}
548 className={`btn-animate btn py-0 px-1 ${
549 this.state.my_vote == 1 ? "text-info" : "text-muted"
551 onClick={linkEvent(this, this.handlePostLike)}
552 aria-label={i18n.t("upvote")}
554 <Icon icon="arrow-up1" classes="icon-inline small" />
557 {this.props.enableDownvotes &&
560 className={`ml-2 btn-animate btn py-0 pl-1 ${
561 this.state.my_vote == -1 ? "text-danger" : "text-muted"
563 onClick={linkEvent(this, this.handlePostDisLike)}
564 data-tippy-content={this.pointsTippy}
565 aria-label={i18n.t("downvote")}
567 <Icon icon="arrow-down1" classes="icon-inline small mr-2" />
568 {this.state.downvotes !== 0 && (
569 <span>{numToSI(this.state.downvotes)}</span>
574 className={`ml-2 btn-animate btn py-0 pl-1 ${
575 this.state.my_vote == -1 ? "text-danger" : "text-muted"
577 onClick={linkEvent(this, this.handlePostDisLike)}
578 aria-label={i18n.t("downvote")}
580 <Icon icon="arrow-down1" classes="icon-inline small" />
585 class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
586 onClick={linkEvent(this, this.handleSavePostClick)}
587 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
589 post_view.saved ? i18n.t("unsave") : i18n.t("save")
594 classes={`icon-inline ${post_view.saved && "text-warning"}`}
598 {!this.state.showMoreMobile && this.showBody && (
600 class="btn btn-link btn-animate text-muted py-0"
601 onClick={linkEvent(this, this.handleShowMoreMobile)}
602 aria-label={i18n.t("more")}
603 data-tippy-content={i18n.t("more")}
605 <Icon icon="more-vertical" classes="icon-inline" />
608 {this.state.showMoreMobile && this.postActions(mobile)}
616 let dupes = this.props.duplicates;
619 dupes.length > 0 && (
620 <ul class="list-inline mb-1 small text-muted">
622 <li className="list-inline-item mr-2">
623 {i18n.t("cross_posted_to")}
626 <li className="list-inline-item mr-2">
627 <Link to={`/post/${pv.post.id}`}>
630 : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
640 postActions(mobile = false) {
641 let post_view = this.props.post_view;
643 UserService.Instance.myUserInfo && (
649 class="btn btn-link btn-animate text-muted py-0 pl-0"
650 onClick={linkEvent(this, this.handleSavePostClick)}
652 post_view.saved ? i18n.t("unsave") : i18n.t("save")
655 post_view.saved ? i18n.t("unsave") : i18n.t("save")
660 classes={`icon-inline ${post_view.saved && "text-warning"}`}
665 className="btn btn-link btn-animate text-muted py-0"
666 to={`/create_post${this.crossPostParams}`}
667 title={i18n.t("cross_post")}
669 <Icon icon="copy" classes="icon-inline" />
674 class="btn btn-link btn-animate text-muted py-0"
675 onClick={linkEvent(this, this.handleShowReportDialog)}
676 data-tippy-content={i18n.t("show_report_dialog")}
677 aria-label={i18n.t("show_report_dialog")}
679 <Icon icon="flag" classes="icon-inline" />
682 class="btn btn-link btn-animate text-muted py-0"
683 onClick={linkEvent(this, this.handleBlockUserClick)}
684 data-tippy-content={i18n.t("block_user")}
685 aria-label={i18n.t("block_user")}
687 <Icon icon="slash" classes="icon-inline" />
693 {this.myPost && this.showBody && (
696 class="btn btn-link btn-animate text-muted py-0"
697 onClick={linkEvent(this, this.handleEditClick)}
698 data-tippy-content={i18n.t("edit")}
699 aria-label={i18n.t("edit")}
701 <Icon icon="edit" classes="icon-inline" />
704 class="btn btn-link btn-animate text-muted py-0"
705 onClick={linkEvent(this, this.handleDeleteClick)}
707 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
710 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
715 classes={`icon-inline ${
716 post_view.post.deleted && "text-danger"
723 {!this.state.showAdvanced && this.showBody ? (
725 class="btn btn-link btn-animate text-muted py-0"
726 onClick={linkEvent(this, this.handleShowAdvanced)}
727 data-tippy-content={i18n.t("more")}
728 aria-label={i18n.t("more")}
730 <Icon icon="more-vertical" classes="icon-inline" />
734 {this.showBody && post_view.post.body && (
736 class="btn btn-link btn-animate text-muted py-0"
737 onClick={linkEvent(this, this.handleViewSource)}
738 data-tippy-content={i18n.t("view_source")}
739 aria-label={i18n.t("view_source")}
743 classes={`icon-inline ${
744 this.state.viewSource && "text-success"
749 {this.canModOnSelf && (
752 class="btn btn-link btn-animate text-muted py-0"
753 onClick={linkEvent(this, this.handleModLock)}
755 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
758 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
763 classes={`icon-inline ${
764 post_view.post.locked && "text-danger"
769 class="btn btn-link btn-animate text-muted py-0"
770 onClick={linkEvent(this, this.handleModSticky)}
772 post_view.post.stickied
777 post_view.post.stickied
784 classes={`icon-inline ${
785 post_view.post.stickied && "text-success"
791 {/* Mods can ban from community, and appoint as mods to community */}
792 {(this.canMod || this.canAdmin) &&
793 (!post_view.post.removed ? (
795 class="btn btn-link btn-animate text-muted py-0"
796 onClick={linkEvent(this, this.handleModRemoveShow)}
797 aria-label={i18n.t("remove")}
803 class="btn btn-link btn-animate text-muted py-0"
804 onClick={linkEvent(this, this.handleModRemoveSubmit)}
805 aria-label={i18n.t("restore")}
813 (!post_view.creator_banned_from_community ? (
815 class="btn btn-link btn-animate text-muted py-0"
818 this.handleModBanFromCommunityShow
820 aria-label={i18n.t("ban")}
826 class="btn btn-link btn-animate text-muted py-0"
829 this.handleModBanFromCommunitySubmit
831 aria-label={i18n.t("unban")}
836 {!post_view.creator_banned_from_community && (
838 class="btn btn-link btn-animate text-muted py-0"
839 onClick={linkEvent(this, this.handleAddModToCommunity)}
842 ? i18n.t("remove_as_mod")
843 : i18n.t("appoint_as_mod")
847 ? i18n.t("remove_as_mod")
848 : i18n.t("appoint_as_mod")}
853 {/* Community creators and admins can transfer community to another mod */}
854 {(this.amCommunityCreator || this.canAdmin) &&
856 (!this.state.showConfirmTransferCommunity ? (
858 class="btn btn-link btn-animate text-muted py-0"
861 this.handleShowConfirmTransferCommunity
863 aria-label={i18n.t("transfer_community")}
865 {i18n.t("transfer_community")}
870 class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
871 aria-label={i18n.t("are_you_sure")}
873 {i18n.t("are_you_sure")}
876 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
877 aria-label={i18n.t("yes")}
878 onClick={linkEvent(this, this.handleTransferCommunity)}
883 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
886 this.handleCancelShowConfirmTransferCommunity
888 aria-label={i18n.t("no")}
894 {/* Admins can ban from all, and appoint other admins */}
898 (!post_view.creator.banned ? (
900 class="btn btn-link btn-animate text-muted py-0"
901 onClick={linkEvent(this, this.handleModBanShow)}
902 aria-label={i18n.t("ban_from_site")}
904 {i18n.t("ban_from_site")}
908 class="btn btn-link btn-animate text-muted py-0"
909 onClick={linkEvent(this, this.handleModBanSubmit)}
910 aria-label={i18n.t("unban_from_site")}
912 {i18n.t("unban_from_site")}
915 {!post_view.creator.banned && post_view.creator.local && (
917 class="btn btn-link btn-animate text-muted py-0"
918 onClick={linkEvent(this, this.handleAddAdmin)}
921 ? i18n.t("remove_as_admin")
922 : i18n.t("appoint_as_admin")
926 ? i18n.t("remove_as_admin")
927 : i18n.t("appoint_as_admin")}
932 {/* Site Creator can transfer to another admin */}
933 {this.amSiteCreator &&
935 (!this.state.showConfirmTransferSite ? (
937 class="btn btn-link btn-animate text-muted py-0"
940 this.handleShowConfirmTransferSite
942 aria-label={i18n.t("transfer_site")}
944 {i18n.t("transfer_site")}
949 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
950 aria-label={i18n.t("are_you_sure")}
952 {i18n.t("are_you_sure")}
955 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
956 onClick={linkEvent(this, this.handleTransferSite)}
957 aria-label={i18n.t("yes")}
962 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
965 this.handleCancelShowConfirmTransferSite
967 aria-label={i18n.t("no")}
980 removeAndBanDialogs() {
981 let post = this.props.post_view;
984 {this.state.showRemoveDialog && (
987 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
989 <label class="sr-only" htmlFor="post-listing-remove-reason">
994 id="post-listing-remove-reason"
995 class="form-control mr-2"
996 placeholder={i18n.t("reason")}
997 value={this.state.removeReason}
998 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
1002 class="btn btn-secondary"
1003 aria-label={i18n.t("remove_post")}
1005 {i18n.t("remove_post")}
1009 {this.state.showBanDialog && (
1010 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
1011 <div class="form-group row">
1012 <label class="col-form-label" htmlFor="post-listing-ban-reason">
1017 id="post-listing-ban-reason"
1018 class="form-control mr-2"
1019 placeholder={i18n.t("reason")}
1020 value={this.state.banReason}
1021 onInput={linkEvent(this, this.handleModBanReasonChange)}
1023 <div class="form-group">
1024 <div class="form-check">
1026 class="form-check-input"
1027 id="mod-ban-remove-data"
1029 checked={this.state.removeData}
1030 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1033 class="form-check-label"
1034 htmlFor="mod-ban-remove-data"
1035 title={i18n.t("remove_content_more")}
1037 {i18n.t("remove_content")}
1042 {/* TODO hold off on expires until later */}
1043 {/* <div class="form-group row"> */}
1044 {/* <label class="col-form-label">Expires</label> */}
1045 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1047 <div class="form-group row">
1050 class="btn btn-secondary"
1051 aria-label={i18n.t("ban")}
1053 {i18n.t("ban")} {post.creator.name}
1058 {this.state.showReportDialog && (
1061 onSubmit={linkEvent(this, this.handleReportSubmit)}
1063 <label class="sr-only" htmlFor="post-report-reason">
1068 id="post-report-reason"
1069 class="form-control mr-2"
1070 placeholder={i18n.t("reason")}
1072 value={this.state.reportReason}
1073 onInput={linkEvent(this, this.handleReportReasonChange)}
1077 class="btn btn-secondary"
1078 aria-label={i18n.t("create_report")}
1080 {i18n.t("create_report")}
1089 let post = this.props.post_view.post;
1090 return post.thumbnail_url || isImage(post.url) ? (
1092 <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1093 {this.postTitleLine()}
1096 {/* Post body prev or thumbnail */}
1097 {!this.state.imageExpanded && this.thumbnail()}
1101 this.postTitleLine()
1105 showMobilePreview() {
1106 let post = this.props.post_view.post;
1111 className="md-div mb-1"
1112 dangerouslySetInnerHTML={{
1113 __html: md.render(previewLines(post.body)),
1123 {/* The mobile view*/}
1124 <div class="d-block d-sm-none">
1126 <div class="col-12">
1127 {this.createdLine()}
1129 {/* If it has a thumbnail, do a right aligned thumbnail */}
1130 {this.mobileThumbnail()}
1132 {/* Show a preview of the post body */}
1133 {this.showMobilePreview()}
1135 {this.commentsLine(true)}
1136 {this.duplicatesLine()}
1137 {this.removeAndBanDialogs()}
1142 {/* The larger view*/}
1143 <div class="d-none d-sm-block">
1146 {!this.state.imageExpanded && (
1147 <div class="col-sm-2 pr-0">
1148 <div class="">{this.thumbnail()}</div>
1153 this.state.imageExpanded ? "col-12" : "col-12 col-sm-9"
1157 <div className="col-12">
1158 {this.postTitleLine()}
1159 {this.createdLine()}
1160 {this.commentsLine()}
1161 {this.duplicatesLine()}
1162 {this.postActions()}
1163 {this.removeAndBanDialogs()}
1173 private get myPost(): boolean {
1175 UserService.Instance.myUserInfo &&
1176 this.props.post_view.creator.id ==
1177 UserService.Instance.myUserInfo.local_user_view.person.id
1181 get isMod(): boolean {
1183 this.props.moderators &&
1185 this.props.moderators.map(m => m.moderator.id),
1186 this.props.post_view.creator.id
1191 get isAdmin(): boolean {
1193 this.props.admins &&
1195 this.props.admins.map(a => a.person.id),
1196 this.props.post_view.creator.id
1201 get canMod(): boolean {
1202 if (this.props.admins && this.props.moderators) {
1203 let adminsThenMods = this.props.admins
1204 .map(a => a.person.id)
1205 .concat(this.props.moderators.map(m => m.moderator.id));
1208 UserService.Instance.myUserInfo,
1210 this.props.post_view.creator.id
1217 get canModOnSelf(): boolean {
1218 if (this.props.admins && this.props.moderators) {
1219 let adminsThenMods = this.props.admins
1220 .map(a => a.person.id)
1221 .concat(this.props.moderators.map(m => m.moderator.id));
1224 UserService.Instance.myUserInfo,
1226 this.props.post_view.creator.id,
1234 get canAdmin(): boolean {
1236 this.props.admins &&
1238 UserService.Instance.myUserInfo,
1239 this.props.admins.map(a => a.person.id),
1240 this.props.post_view.creator.id
1245 get amCommunityCreator(): boolean {
1247 this.props.moderators &&
1248 UserService.Instance.myUserInfo &&
1249 this.props.post_view.creator.id !=
1250 UserService.Instance.myUserInfo.local_user_view.person.id &&
1251 UserService.Instance.myUserInfo.local_user_view.person.id ==
1252 this.props.moderators[0].moderator.id
1256 get amSiteCreator(): boolean {
1258 this.props.admins &&
1259 UserService.Instance.myUserInfo &&
1260 this.props.post_view.creator.id !=
1261 UserService.Instance.myUserInfo.local_user_view.person.id &&
1262 UserService.Instance.myUserInfo.local_user_view.person.id ==
1263 this.props.admins[0].person.id
1267 handlePostLike(i: PostListing, event: any) {
1268 event.preventDefault();
1269 if (!UserService.Instance.myUserInfo) {
1270 this.context.router.history.push(`/login`);
1273 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1275 if (i.state.my_vote == 1) {
1278 } else if (i.state.my_vote == -1) {
1279 i.state.downvotes--;
1287 i.state.my_vote = new_vote;
1289 let form: CreatePostLike = {
1290 post_id: i.props.post_view.post.id,
1291 score: i.state.my_vote,
1295 WebSocketService.Instance.send(wsClient.likePost(form));
1296 i.setState(i.state);
1300 handlePostDisLike(i: PostListing, event: any) {
1301 event.preventDefault();
1302 if (!UserService.Instance.myUserInfo) {
1303 this.context.router.history.push(`/login`);
1306 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1308 if (i.state.my_vote == 1) {
1311 i.state.downvotes++;
1312 } else if (i.state.my_vote == -1) {
1313 i.state.downvotes--;
1316 i.state.downvotes++;
1320 i.state.my_vote = new_vote;
1322 let form: CreatePostLike = {
1323 post_id: i.props.post_view.post.id,
1324 score: i.state.my_vote,
1328 WebSocketService.Instance.send(wsClient.likePost(form));
1329 i.setState(i.state);
1333 handleEditClick(i: PostListing) {
1334 i.state.showEdit = true;
1335 i.setState(i.state);
1338 handleEditCancel() {
1339 this.state.showEdit = false;
1340 this.setState(this.state);
1343 // The actual editing is done in the recieve for post
1345 this.state.showEdit = false;
1346 this.setState(this.state);
1349 handleShowReportDialog(i: PostListing) {
1350 i.state.showReportDialog = !i.state.showReportDialog;
1351 i.setState(this.state);
1354 handleReportReasonChange(i: PostListing, event: any) {
1355 i.state.reportReason = event.target.value;
1356 i.setState(i.state);
1359 handleReportSubmit(i: PostListing, event: any) {
1360 event.preventDefault();
1361 let form: CreatePostReport = {
1362 post_id: i.props.post_view.post.id,
1363 reason: i.state.reportReason,
1366 WebSocketService.Instance.send(wsClient.createPostReport(form));
1368 i.state.showReportDialog = false;
1369 i.setState(i.state);
1372 handleBlockUserClick(i: PostListing) {
1373 let blockUserForm: BlockPerson = {
1374 person_id: i.props.post_view.creator.id,
1378 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1381 handleDeleteClick(i: PostListing) {
1382 let deleteForm: DeletePost = {
1383 post_id: i.props.post_view.post.id,
1384 deleted: !i.props.post_view.post.deleted,
1387 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1390 handleSavePostClick(i: PostListing) {
1392 i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1393 let form: SavePost = {
1394 post_id: i.props.post_view.post.id,
1399 WebSocketService.Instance.send(wsClient.savePost(form));
1402 get crossPostParams(): string {
1403 let post = this.props.post_view.post;
1404 let params = `?title=${encodeURIComponent(post.name)}`;
1407 params += `&url=${encodeURIComponent(post.url)}`;
1410 params += `&body=${encodeURIComponent(this.crossPostBody())}`;
1415 crossPostBody(): string {
1416 let post = this.props.post_view.post;
1417 let body = `${i18n.t("cross_posted_from")} ${
1419 }\n\n${post.body.replace(/^/gm, "> ")}`;
1423 get showBody(): boolean {
1424 return this.props.showBody || this.state.showBody;
1427 handleModRemoveShow(i: PostListing) {
1428 i.state.showRemoveDialog = !i.state.showRemoveDialog;
1429 i.state.showBanDialog = false;
1430 i.setState(i.state);
1433 handleModRemoveReasonChange(i: PostListing, event: any) {
1434 i.state.removeReason = event.target.value;
1435 i.setState(i.state);
1438 handleModRemoveDataChange(i: PostListing, event: any) {
1439 i.state.removeData = event.target.checked;
1440 i.setState(i.state);
1443 handleModRemoveSubmit(i: PostListing, event: any) {
1444 event.preventDefault();
1445 let form: RemovePost = {
1446 post_id: i.props.post_view.post.id,
1447 removed: !i.props.post_view.post.removed,
1448 reason: i.state.removeReason,
1451 WebSocketService.Instance.send(wsClient.removePost(form));
1453 i.state.showRemoveDialog = false;
1454 i.setState(i.state);
1457 handleModLock(i: PostListing) {
1458 let form: LockPost = {
1459 post_id: i.props.post_view.post.id,
1460 locked: !i.props.post_view.post.locked,
1463 WebSocketService.Instance.send(wsClient.lockPost(form));
1466 handleModSticky(i: PostListing) {
1467 let form: StickyPost = {
1468 post_id: i.props.post_view.post.id,
1469 stickied: !i.props.post_view.post.stickied,
1472 WebSocketService.Instance.send(wsClient.stickyPost(form));
1475 handleModBanFromCommunityShow(i: PostListing) {
1476 i.state.showBanDialog = true;
1477 i.state.banType = BanType.Community;
1478 i.state.showRemoveDialog = false;
1479 i.setState(i.state);
1482 handleModBanShow(i: PostListing) {
1483 i.state.showBanDialog = true;
1484 i.state.banType = BanType.Site;
1485 i.state.showRemoveDialog = false;
1486 i.setState(i.state);
1489 handleModBanReasonChange(i: PostListing, event: any) {
1490 i.state.banReason = event.target.value;
1491 i.setState(i.state);
1494 handleModBanExpiresChange(i: PostListing, event: any) {
1495 i.state.banExpires = event.target.value;
1496 i.setState(i.state);
1499 handleModBanFromCommunitySubmit(i: PostListing) {
1500 i.state.banType = BanType.Community;
1501 i.setState(i.state);
1502 i.handleModBanBothSubmit(i);
1505 handleModBanSubmit(i: PostListing) {
1506 i.state.banType = BanType.Site;
1507 i.setState(i.state);
1508 i.handleModBanBothSubmit(i);
1511 handleModBanBothSubmit(i: PostListing, event?: any) {
1512 if (event) event.preventDefault();
1514 if (i.state.banType == BanType.Community) {
1515 // If its an unban, restore all their data
1516 let ban = !i.props.post_view.creator_banned_from_community;
1518 i.state.removeData = false;
1520 let form: BanFromCommunity = {
1521 person_id: i.props.post_view.creator.id,
1522 community_id: i.props.post_view.community.id,
1524 remove_data: i.state.removeData,
1525 reason: i.state.banReason,
1526 expires: getUnixTime(i.state.banExpires),
1529 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1531 // If its an unban, restore all their data
1532 let ban = !i.props.post_view.creator.banned;
1534 i.state.removeData = false;
1536 let form: BanPerson = {
1537 person_id: i.props.post_view.creator.id,
1539 remove_data: i.state.removeData,
1540 reason: i.state.banReason,
1541 expires: getUnixTime(i.state.banExpires),
1544 WebSocketService.Instance.send(wsClient.banPerson(form));
1547 i.state.showBanDialog = false;
1548 i.setState(i.state);
1551 handleAddModToCommunity(i: PostListing) {
1552 let form: AddModToCommunity = {
1553 person_id: i.props.post_view.creator.id,
1554 community_id: i.props.post_view.community.id,
1558 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1559 i.setState(i.state);
1562 handleAddAdmin(i: PostListing) {
1563 let form: AddAdmin = {
1564 person_id: i.props.post_view.creator.id,
1568 WebSocketService.Instance.send(wsClient.addAdmin(form));
1569 i.setState(i.state);
1572 handleShowConfirmTransferCommunity(i: PostListing) {
1573 i.state.showConfirmTransferCommunity = true;
1574 i.setState(i.state);
1577 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1578 i.state.showConfirmTransferCommunity = false;
1579 i.setState(i.state);
1582 handleTransferCommunity(i: PostListing) {
1583 let form: TransferCommunity = {
1584 community_id: i.props.post_view.community.id,
1585 person_id: i.props.post_view.creator.id,
1588 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1589 i.state.showConfirmTransferCommunity = false;
1590 i.setState(i.state);
1593 handleShowConfirmTransferSite(i: PostListing) {
1594 i.state.showConfirmTransferSite = true;
1595 i.setState(i.state);
1598 handleCancelShowConfirmTransferSite(i: PostListing) {
1599 i.state.showConfirmTransferSite = false;
1600 i.setState(i.state);
1603 handleTransferSite(i: PostListing) {
1604 let form: TransferSite = {
1605 person_id: i.props.post_view.creator.id,
1608 WebSocketService.Instance.send(wsClient.transferSite(form));
1609 i.state.showConfirmTransferSite = false;
1610 i.setState(i.state);
1613 handleImageExpandClick(i: PostListing, event: any) {
1614 event.preventDefault();
1615 i.state.imageExpanded = !i.state.imageExpanded;
1616 i.setState(i.state);
1619 handleViewSource(i: PostListing) {
1620 i.state.viewSource = !i.state.viewSource;
1621 i.setState(i.state);
1624 handleShowAdvanced(i: PostListing) {
1625 i.state.showAdvanced = !i.state.showAdvanced;
1626 i.setState(i.state);
1630 handleShowMoreMobile(i: PostListing) {
1631 i.state.showMoreMobile = !i.state.showMoreMobile;
1632 i.state.showAdvanced = !i.state.showAdvanced;
1633 i.setState(i.state);
1637 handleShowBody(i: PostListing) {
1638 i.state.showBody = !i.state.showBody;
1639 i.setState(i.state);
1643 get pointsTippy(): string {
1644 let points = i18n.t("number_of_points", {
1645 count: this.state.score,
1646 formattedCount: this.state.score,
1649 let upvotes = i18n.t("number_of_upvotes", {
1650 count: this.state.upvotes,
1651 formattedCount: this.state.upvotes,
1654 let downvotes = i18n.t("number_of_downvotes", {
1655 count: this.state.downvotes,
1656 formattedCount: this.state.downvotes,
1659 return `${points} • ${upvotes} • ${downvotes}`;