1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
9 CommunityModeratorView,
20 } from "lemmy-js-client";
21 import { externalHost } from "../../env";
22 import { i18n } from "../../i18next";
23 import { BanType } from "../../interfaces";
24 import { UserService, WebSocketService } from "../../services";
40 import { Icon } from "../common/icon";
41 import { MomentTime } from "../common/moment-time";
42 import { PictrsImage } from "../common/pictrs-image";
43 import { CommunityLink } from "../community/community-link";
44 import { PersonListing } from "../person/person-listing";
45 import { MetadataCard } from "./metadata-card";
46 import { PostForm } from "./post-form";
48 interface PostListingState {
50 showRemoveDialog: boolean;
52 showBanDialog: boolean;
57 showConfirmTransferSite: boolean;
58 showConfirmTransferCommunity: boolean;
59 imageExpanded: boolean;
61 showAdvanced: boolean;
62 showMoreMobile: boolean;
70 interface PostListingProps {
72 duplicates?: PostView[];
73 showCommunity?: boolean;
75 moderators?: CommunityModeratorView[];
76 admins?: PersonViewSafe[];
77 enableDownvotes: boolean;
81 export class PostListing extends Component<PostListingProps, PostListingState> {
82 private emptyState: PostListingState = {
84 showRemoveDialog: false,
90 banType: BanType.Community,
91 showConfirmTransferSite: false,
92 showConfirmTransferCommunity: false,
96 showMoreMobile: false,
98 my_vote: this.props.post_view.my_vote,
99 score: this.props.post_view.counts.score,
100 upvotes: this.props.post_view.counts.upvotes,
101 downvotes: this.props.post_view.counts.downvotes,
104 constructor(props: any, context: any) {
105 super(props, context);
107 this.state = this.emptyState;
108 this.handlePostLike = this.handlePostLike.bind(this);
109 this.handlePostDisLike = this.handlePostDisLike.bind(this);
110 this.handleEditPost = this.handleEditPost.bind(this);
111 this.handleEditCancel = this.handleEditCancel.bind(this);
114 componentWillReceiveProps(nextProps: PostListingProps) {
115 this.state.my_vote = nextProps.post_view.my_vote;
116 this.state.upvotes = nextProps.post_view.counts.upvotes;
117 this.state.downvotes = nextProps.post_view.counts.downvotes;
118 this.state.score = nextProps.post_view.counts.score;
119 if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
120 this.state.imageExpanded = false;
122 this.setState(this.state);
128 {!this.state.showEdit ? (
136 post_view={this.props.post_view}
137 onEdit={this.handleEditPost}
138 onCancel={this.handleEditCancel}
139 enableNsfw={this.props.enableNsfw}
140 enableDownvotes={this.props.enableDownvotes}
149 let post = this.props.post_view.post;
153 {post.url && this.showBody && post.embed_title && (
154 <MetadataCard post={post} />
158 (this.state.viewSource ? (
159 <pre>{post.body}</pre>
163 dangerouslySetInnerHTML={mdToHtml(post.body)}
171 imgThumb(src: string) {
172 let post_view = this.props.post_view;
178 nsfw={post_view.post.nsfw || post_view.community.nsfw}
183 getImageSrc(): string {
184 let post = this.props.post_view.post;
185 if (isImage(post.url)) {
186 if (post.url.includes("pictrs")) {
188 } else if (post.thumbnail_url) {
189 return post.thumbnail_url;
193 } else if (post.thumbnail_url) {
194 return post.thumbnail_url;
201 let post = this.props.post_view.post;
203 if (isImage(post.url)) {
206 class="float-right text-body pointer d-inline-block position-relative mb-2"
207 data-tippy-content={i18n.t("expand_here")}
208 onClick={linkEvent(this, this.handleImageExpandClick)}
210 aria-label={i18n.t("expand_here")}
212 {this.imgThumb(this.getImageSrc())}
213 <Icon icon="image" classes="mini-overlay" />
216 } else if (post.thumbnail_url) {
219 class="float-right text-body d-inline-block position-relative mb-2"
224 {this.imgThumb(this.getImageSrc())}
225 <Icon icon="external-link" classes="mini-overlay" />
228 } else if (post.url) {
229 if (isVideo(post.url)) {
231 <div class="embed-responsive embed-responsive-16by9">
237 class="embed-responsive-item"
239 <source src={post.url} type="video/mp4" />
246 className="text-body"
251 <div class="thumbnail rounded bg-light d-flex justify-content-center">
252 <Icon icon="external-link" classes="d-flex align-items-center" />
260 className="text-body"
261 to={`/post/${post.id}`}
262 title={i18n.t("comments")}
264 <div class="thumbnail rounded bg-light d-flex justify-content-center">
265 <Icon icon="message-square" classes="d-flex align-items-center" />
273 let post_view = this.props.post_view;
275 <ul class="list-inline mb-1 text-muted small">
276 <li className="list-inline-item">
277 <PersonListing person={post_view.creator} />
280 <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
283 <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
285 {(post_view.creator_banned_from_community ||
286 post_view.creator.banned) && (
287 <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
289 {post_view.creator_blocked && (
290 <span className="mx-1 badge badge-danger">{"blocked"}</span>
292 {this.props.showCommunity && (
294 <span class="mx-1"> {i18n.t("to")} </span>
295 <CommunityLink community={post_view.community} />
299 <li className="list-inline-item">•</li>
300 {post_view.post.url && !(hostname(post_view.post.url) == externalHost) && (
302 <li className="list-inline-item">
304 className="text-muted font-italic"
305 href={post_view.post.url}
306 title={post_view.post.url}
309 {hostname(post_view.post.url)}
312 <li className="list-inline-item">•</li>
315 <li className="list-inline-item">
317 <MomentTime data={post_view.post} />
320 {post_view.post.body && (
322 <li className="list-inline-item">•</li>
323 <li className="list-inline-item">
325 className="text-muted btn btn-sm btn-link p-0"
326 data-tippy-content={md.render(
327 previewLines(post_view.post.body)
329 data-tippy-allowHtml={true}
330 onClick={linkEvent(this, this.handleShowBody)}
332 <Icon icon="book-open" classes="icon-inline mr-1" />
343 <div className={`vote-bar col-1 pr-0 small text-center`}>
345 className={`btn-animate btn btn-link p-0 ${
346 this.state.my_vote == 1 ? "text-info" : "text-muted"
348 onClick={linkEvent(this, this.handlePostLike)}
349 data-tippy-content={i18n.t("upvote")}
350 aria-label={i18n.t("upvote")}
352 <Icon icon="arrow-up1" classes="upvote" />
356 class={`unselectable pointer font-weight-bold text-muted px-1`}
357 data-tippy-content={this.pointsTippy}
362 <div class="p-1"></div>
364 {this.props.enableDownvotes && (
366 className={`btn-animate btn btn-link p-0 ${
367 this.state.my_vote == -1 ? "text-danger" : "text-muted"
369 onClick={linkEvent(this, this.handlePostDisLike)}
370 data-tippy-content={i18n.t("downvote")}
371 aria-label={i18n.t("downvote")}
373 <Icon icon="arrow-down1" classes="downvote" />
381 let post = this.props.post_view.post;
383 <div className="post-title overflow-hidden">
385 {this.showBody && post.url ? (
387 className={!post.stickied ? "text-body" : "text-primary"}
396 className={!post.stickied ? "text-body" : "text-primary"}
397 to={`/post/${post.id}`}
398 title={i18n.t("comments")}
403 {(isImage(post.url) || post.thumbnail_url) &&
404 (!this.state.imageExpanded ? (
406 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
407 data-tippy-content={i18n.t("expand_here")}
408 onClick={linkEvent(this, this.handleImageExpandClick)}
410 <Icon icon="plus-square" classes="icon-inline" />
415 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
416 onClick={linkEvent(this, this.handleImageExpandClick)}
418 <Icon icon="minus-square" classes="icon-inline" />
422 class="btn btn-link d-inline-block"
423 onClick={linkEvent(this, this.handleImageExpandClick)}
425 <PictrsImage src={this.getImageSrc()} />
431 <small className="ml-2 text-muted font-italic">
437 className="unselectable pointer ml-2 text-muted font-italic"
438 data-tippy-content={i18n.t("deleted")}
440 <Icon icon="trash" classes="icon-inline text-danger" />
445 className="unselectable pointer ml-2 text-muted font-italic"
446 data-tippy-content={i18n.t("locked")}
448 <Icon icon="lock" classes="icon-inline text-danger" />
453 className="unselectable pointer ml-2 text-muted font-italic"
454 data-tippy-content={i18n.t("stickied")}
456 <Icon icon="pin" classes="icon-inline text-primary" />
460 <small className="ml-2 text-muted font-italic">
469 commentsLine(mobile = false) {
470 let post_view = this.props.post_view;
472 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
473 <button class="btn btn-link text-muted p-0">
475 className="text-muted small"
476 title={i18n.t("number_of_comments", {
477 count: post_view.counts.comments,
479 to={`/post/${post_view.post.id}?scrollToComments=true`}
481 <Icon icon="message-square" classes="icon-inline mr-1" />
482 {i18n.t("number_of_comments", {
483 count: post_view.counts.comments,
489 {this.state.downvotes !== 0 && showScores() && (
491 class="btn text-muted py-0 pr-0"
492 data-tippy-content={this.pointsTippy}
493 aria-label={i18n.t("downvote")}
496 <Icon icon="arrow-down1" classes="icon-inline mr-1" />
497 <span>{this.state.downvotes}</span>
503 class="btn btn-link btn-animate text-muted py-0"
504 onClick={linkEvent(this, this.handleSavePostClick)}
506 post_view.saved ? i18n.t("unsave") : i18n.t("save")
508 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
513 classes={`icon-inline ${post_view.saved && "text-warning"}`}
520 {/* This is an expanding spacer for mobile */}
521 <div className="flex-grow-1"></div>
527 className={`btn-animate btn py-0 px-1 ${
528 this.state.my_vote == 1 ? "text-info" : "text-muted"
530 data-tippy-content={this.pointsTippy}
531 onClick={linkEvent(this, this.handlePostLike)}
532 aria-label={i18n.t("upvote")}
534 <Icon icon="arrow-up1" classes="icon-inline small mr-2" />
539 className={`btn-animate btn py-0 px-1 ${
540 this.state.my_vote == 1 ? "text-info" : "text-muted"
542 onClick={linkEvent(this, this.handlePostLike)}
543 aria-label={i18n.t("upvote")}
545 <Icon icon="arrow-up1" classes="icon-inline small" />
548 {this.props.enableDownvotes &&
551 className={`ml-2 btn-animate btn py-0 pl-1 ${
552 this.state.my_vote == -1 ? "text-danger" : "text-muted"
554 onClick={linkEvent(this, this.handlePostDisLike)}
555 data-tippy-content={this.pointsTippy}
556 aria-label={i18n.t("downvote")}
558 <Icon icon="arrow-down1" classes="icon-inline small mr-2" />
559 {this.state.downvotes !== 0 && (
560 <span>{this.state.downvotes}</span>
565 className={`ml-2 btn-animate btn py-0 pl-1 ${
566 this.state.my_vote == -1 ? "text-danger" : "text-muted"
568 onClick={linkEvent(this, this.handlePostDisLike)}
569 aria-label={i18n.t("downvote")}
571 <Icon icon="arrow-down1" classes="icon-inline small" />
576 class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
577 onClick={linkEvent(this, this.handleSavePostClick)}
578 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
580 post_view.saved ? i18n.t("unsave") : i18n.t("save")
585 classes={`icon-inline ${post_view.saved && "text-warning"}`}
589 {!this.state.showMoreMobile && this.showBody && (
591 class="btn btn-link btn-animate text-muted py-0"
592 onClick={linkEvent(this, this.handleShowMoreMobile)}
593 aria-label={i18n.t("more")}
594 data-tippy-content={i18n.t("more")}
596 <Icon icon="more-vertical" classes="icon-inline" />
599 {this.state.showMoreMobile && this.postActions(mobile)}
607 let dupes = this.props.duplicates;
610 dupes.length > 0 && (
611 <ul class="list-inline mb-1 small text-muted">
613 <li className="list-inline-item mr-2">
614 {i18n.t("cross_posted_to")}
617 <li className="list-inline-item mr-2">
618 <Link to={`/post/${pv.post.id}`}>
621 : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
631 postActions(mobile = false) {
632 let post_view = this.props.post_view;
634 UserService.Instance.myUserInfo && (
640 class="btn btn-link btn-animate text-muted py-0 pl-0"
641 onClick={linkEvent(this, this.handleSavePostClick)}
643 post_view.saved ? i18n.t("unsave") : i18n.t("save")
646 post_view.saved ? i18n.t("unsave") : i18n.t("save")
651 classes={`icon-inline ${post_view.saved && "text-warning"}`}
656 className="btn btn-link btn-animate text-muted py-0"
657 to={`/create_post${this.crossPostParams}`}
658 title={i18n.t("cross_post")}
660 <Icon icon="copy" classes="icon-inline" />
664 class="btn btn-link btn-animate text-muted py-0"
665 onClick={linkEvent(this, this.handleBlockUserClick)}
666 data-tippy-content={i18n.t("block_user")}
667 aria-label={i18n.t("block_user")}
669 <Icon icon="slash" classes="icon-inline" />
674 {this.myPost && this.showBody && (
677 class="btn btn-link btn-animate text-muted py-0"
678 onClick={linkEvent(this, this.handleEditClick)}
679 data-tippy-content={i18n.t("edit")}
680 aria-label={i18n.t("edit")}
682 <Icon icon="edit" classes="icon-inline" />
685 class="btn btn-link btn-animate text-muted py-0"
686 onClick={linkEvent(this, this.handleDeleteClick)}
688 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
691 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
696 classes={`icon-inline ${
697 post_view.post.deleted && "text-danger"
704 {!this.state.showAdvanced && this.showBody ? (
706 class="btn btn-link btn-animate text-muted py-0"
707 onClick={linkEvent(this, this.handleShowAdvanced)}
708 data-tippy-content={i18n.t("more")}
709 aria-label={i18n.t("more")}
711 <Icon icon="more-vertical" classes="icon-inline" />
715 {this.showBody && post_view.post.body && (
717 class="btn btn-link btn-animate text-muted py-0"
718 onClick={linkEvent(this, this.handleViewSource)}
719 data-tippy-content={i18n.t("view_source")}
720 aria-label={i18n.t("view_source")}
724 classes={`icon-inline ${
725 this.state.viewSource && "text-success"
730 {this.canModOnSelf && (
733 class="btn btn-link btn-animate text-muted py-0"
734 onClick={linkEvent(this, this.handleModLock)}
736 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
739 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
744 classes={`icon-inline ${
745 post_view.post.locked && "text-danger"
750 class="btn btn-link btn-animate text-muted py-0"
751 onClick={linkEvent(this, this.handleModSticky)}
753 post_view.post.stickied
758 post_view.post.stickied
765 classes={`icon-inline ${
766 post_view.post.stickied && "text-success"
772 {/* Mods can ban from community, and appoint as mods to community */}
773 {(this.canMod || this.canAdmin) &&
774 (!post_view.post.removed ? (
776 class="btn btn-link btn-animate text-muted py-0"
777 onClick={linkEvent(this, this.handleModRemoveShow)}
778 aria-label={i18n.t("remove")}
784 class="btn btn-link btn-animate text-muted py-0"
785 onClick={linkEvent(this, this.handleModRemoveSubmit)}
786 aria-label={i18n.t("restore")}
794 (!post_view.creator_banned_from_community ? (
796 class="btn btn-link btn-animate text-muted py-0"
799 this.handleModBanFromCommunityShow
801 aria-label={i18n.t("ban")}
807 class="btn btn-link btn-animate text-muted py-0"
810 this.handleModBanFromCommunitySubmit
812 aria-label={i18n.t("unban")}
817 {!post_view.creator_banned_from_community && (
819 class="btn btn-link btn-animate text-muted py-0"
820 onClick={linkEvent(this, this.handleAddModToCommunity)}
823 ? i18n.t("remove_as_mod")
824 : i18n.t("appoint_as_mod")
828 ? i18n.t("remove_as_mod")
829 : i18n.t("appoint_as_mod")}
834 {/* Community creators and admins can transfer community to another mod */}
835 {(this.amCommunityCreator || this.canAdmin) &&
837 (!this.state.showConfirmTransferCommunity ? (
839 class="btn btn-link btn-animate text-muted py-0"
842 this.handleShowConfirmTransferCommunity
844 aria-label={i18n.t("transfer_community")}
846 {i18n.t("transfer_community")}
851 class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
852 aria-label={i18n.t("are_you_sure")}
854 {i18n.t("are_you_sure")}
857 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
858 aria-label={i18n.t("yes")}
859 onClick={linkEvent(this, this.handleTransferCommunity)}
864 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
867 this.handleCancelShowConfirmTransferCommunity
869 aria-label={i18n.t("no")}
875 {/* Admins can ban from all, and appoint other admins */}
879 (!post_view.creator.banned ? (
881 class="btn btn-link btn-animate text-muted py-0"
882 onClick={linkEvent(this, this.handleModBanShow)}
883 aria-label={i18n.t("ban_from_site")}
885 {i18n.t("ban_from_site")}
889 class="btn btn-link btn-animate text-muted py-0"
890 onClick={linkEvent(this, this.handleModBanSubmit)}
891 aria-label={i18n.t("unban_from_site")}
893 {i18n.t("unban_from_site")}
896 {!post_view.creator.banned && post_view.creator.local && (
898 class="btn btn-link btn-animate text-muted py-0"
899 onClick={linkEvent(this, this.handleAddAdmin)}
902 ? i18n.t("remove_as_admin")
903 : i18n.t("appoint_as_admin")
907 ? i18n.t("remove_as_admin")
908 : i18n.t("appoint_as_admin")}
913 {/* Site Creator can transfer to another admin */}
914 {this.amSiteCreator &&
916 (!this.state.showConfirmTransferSite ? (
918 class="btn btn-link btn-animate text-muted py-0"
921 this.handleShowConfirmTransferSite
923 aria-label={i18n.t("transfer_site")}
925 {i18n.t("transfer_site")}
930 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
931 aria-label={i18n.t("are_you_sure")}
933 {i18n.t("are_you_sure")}
936 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
937 onClick={linkEvent(this, this.handleTransferSite)}
938 aria-label={i18n.t("yes")}
943 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
946 this.handleCancelShowConfirmTransferSite
948 aria-label={i18n.t("no")}
961 removeAndBanDialogs() {
962 let post = this.props.post_view;
965 {this.state.showRemoveDialog && (
968 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
970 <label class="sr-only" htmlFor="post-listing-remove-reason">
975 id="post-listing-remove-reason"
976 class="form-control mr-2"
977 placeholder={i18n.t("reason")}
978 value={this.state.removeReason}
979 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
983 class="btn btn-secondary"
984 aria-label={i18n.t("remove_post")}
986 {i18n.t("remove_post")}
990 {this.state.showBanDialog && (
991 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
992 <div class="form-group row">
993 <label class="col-form-label" htmlFor="post-listing-ban-reason">
998 id="post-listing-ban-reason"
999 class="form-control mr-2"
1000 placeholder={i18n.t("reason")}
1001 value={this.state.banReason}
1002 onInput={linkEvent(this, this.handleModBanReasonChange)}
1004 <div class="form-group">
1005 <div class="form-check">
1007 class="form-check-input"
1008 id="mod-ban-remove-data"
1010 checked={this.state.removeData}
1011 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1014 class="form-check-label"
1015 htmlFor="mod-ban-remove-data"
1016 title={i18n.t("remove_content_more")}
1018 {i18n.t("remove_content")}
1023 {/* TODO hold off on expires until later */}
1024 {/* <div class="form-group row"> */}
1025 {/* <label class="col-form-label">Expires</label> */}
1026 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1028 <div class="form-group row">
1031 class="btn btn-secondary"
1032 aria-label={i18n.t("ban")}
1034 {i18n.t("ban")} {post.creator.name}
1044 let post = this.props.post_view.post;
1045 return post.thumbnail_url || isImage(post.url) ? (
1047 <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1048 {this.postTitleLine()}
1051 {/* Post body prev or thumbnail */}
1052 {!this.state.imageExpanded && this.thumbnail()}
1056 this.postTitleLine()
1060 showMobilePreview() {
1061 let post = this.props.post_view.post;
1066 className="md-div mb-1"
1067 dangerouslySetInnerHTML={{
1068 __html: md.render(previewLines(post.body)),
1078 {/* The mobile view*/}
1079 <div class="d-block d-sm-none">
1081 <div class="col-12">
1082 {this.createdLine()}
1084 {/* If it has a thumbnail, do a right aligned thumbnail */}
1085 {this.mobileThumbnail()}
1087 {/* Show a preview of the post body */}
1088 {this.showMobilePreview()}
1090 {this.commentsLine(true)}
1091 {this.duplicatesLine()}
1092 {this.removeAndBanDialogs()}
1097 {/* The larger view*/}
1098 <div class="d-none d-sm-block">
1101 {!this.state.imageExpanded && (
1102 <div class="col-sm-2 pr-0">
1103 <div class="">{this.thumbnail()}</div>
1108 this.state.imageExpanded ? "col-12" : "col-12 col-sm-9"
1112 <div className="col-12">
1113 {this.postTitleLine()}
1114 {this.createdLine()}
1115 {this.commentsLine()}
1116 {this.duplicatesLine()}
1117 {this.postActions()}
1118 {this.removeAndBanDialogs()}
1128 private get myPost(): boolean {
1130 UserService.Instance.myUserInfo &&
1131 this.props.post_view.creator.id ==
1132 UserService.Instance.myUserInfo.local_user_view.person.id
1136 get isMod(): boolean {
1138 this.props.moderators &&
1140 this.props.moderators.map(m => m.moderator.id),
1141 this.props.post_view.creator.id
1146 get isAdmin(): boolean {
1148 this.props.admins &&
1150 this.props.admins.map(a => a.person.id),
1151 this.props.post_view.creator.id
1156 get canMod(): boolean {
1157 if (this.props.admins && this.props.moderators) {
1158 let adminsThenMods = this.props.admins
1159 .map(a => a.person.id)
1160 .concat(this.props.moderators.map(m => m.moderator.id));
1163 UserService.Instance.myUserInfo,
1165 this.props.post_view.creator.id
1172 get canModOnSelf(): boolean {
1173 if (this.props.admins && this.props.moderators) {
1174 let adminsThenMods = this.props.admins
1175 .map(a => a.person.id)
1176 .concat(this.props.moderators.map(m => m.moderator.id));
1179 UserService.Instance.myUserInfo,
1181 this.props.post_view.creator.id,
1189 get canAdmin(): boolean {
1191 this.props.admins &&
1193 UserService.Instance.myUserInfo,
1194 this.props.admins.map(a => a.person.id),
1195 this.props.post_view.creator.id
1200 get amCommunityCreator(): boolean {
1202 this.props.moderators &&
1203 UserService.Instance.myUserInfo &&
1204 this.props.post_view.creator.id !=
1205 UserService.Instance.myUserInfo.local_user_view.person.id &&
1206 UserService.Instance.myUserInfo.local_user_view.person.id ==
1207 this.props.moderators[0].moderator.id
1211 get amSiteCreator(): boolean {
1213 this.props.admins &&
1214 UserService.Instance.myUserInfo &&
1215 this.props.post_view.creator.id !=
1216 UserService.Instance.myUserInfo.local_user_view.person.id &&
1217 UserService.Instance.myUserInfo.local_user_view.person.id ==
1218 this.props.admins[0].person.id
1222 handlePostLike(i: PostListing, event: any) {
1223 event.preventDefault();
1224 if (!UserService.Instance.myUserInfo) {
1225 this.context.router.history.push(`/login`);
1228 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1230 if (i.state.my_vote == 1) {
1233 } else if (i.state.my_vote == -1) {
1234 i.state.downvotes--;
1242 i.state.my_vote = new_vote;
1244 let form: CreatePostLike = {
1245 post_id: i.props.post_view.post.id,
1246 score: i.state.my_vote,
1250 WebSocketService.Instance.send(wsClient.likePost(form));
1251 i.setState(i.state);
1255 handlePostDisLike(i: PostListing, event: any) {
1256 event.preventDefault();
1257 if (!UserService.Instance.myUserInfo) {
1258 this.context.router.history.push(`/login`);
1261 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1263 if (i.state.my_vote == 1) {
1266 i.state.downvotes++;
1267 } else if (i.state.my_vote == -1) {
1268 i.state.downvotes--;
1271 i.state.downvotes++;
1275 i.state.my_vote = new_vote;
1277 let form: CreatePostLike = {
1278 post_id: i.props.post_view.post.id,
1279 score: i.state.my_vote,
1283 WebSocketService.Instance.send(wsClient.likePost(form));
1284 i.setState(i.state);
1288 handleEditClick(i: PostListing) {
1289 i.state.showEdit = true;
1290 i.setState(i.state);
1293 handleEditCancel() {
1294 this.state.showEdit = false;
1295 this.setState(this.state);
1298 // The actual editing is done in the recieve for post
1300 this.state.showEdit = false;
1301 this.setState(this.state);
1304 handleBlockUserClick(i: PostListing) {
1305 let blockUserForm: BlockPerson = {
1306 person_id: i.props.post_view.creator.id,
1310 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1313 handleDeleteClick(i: PostListing) {
1314 let deleteForm: DeletePost = {
1315 post_id: i.props.post_view.post.id,
1316 deleted: !i.props.post_view.post.deleted,
1319 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1322 handleSavePostClick(i: PostListing) {
1324 i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1325 let form: SavePost = {
1326 post_id: i.props.post_view.post.id,
1331 WebSocketService.Instance.send(wsClient.savePost(form));
1334 get crossPostParams(): string {
1335 let post = this.props.post_view.post;
1336 let params = `?title=${encodeURIComponent(post.name)}`;
1339 params += `&url=${encodeURIComponent(post.url)}`;
1342 params += `&body=${encodeURIComponent(this.crossPostBody())}`;
1347 crossPostBody(): string {
1348 let post = this.props.post_view.post;
1349 let body = `${i18n.t("cross_posted_from")} ${
1351 }\n\n${post.body.replace(/^/gm, "> ")}`;
1355 get showBody(): boolean {
1356 return this.props.showBody || this.state.showBody;
1359 handleModRemoveShow(i: PostListing) {
1360 i.state.showRemoveDialog = true;
1361 i.setState(i.state);
1364 handleModRemoveReasonChange(i: PostListing, event: any) {
1365 i.state.removeReason = event.target.value;
1366 i.setState(i.state);
1369 handleModRemoveDataChange(i: PostListing, event: any) {
1370 i.state.removeData = event.target.checked;
1371 i.setState(i.state);
1374 handleModRemoveSubmit(i: PostListing, event: any) {
1375 event.preventDefault();
1376 let form: RemovePost = {
1377 post_id: i.props.post_view.post.id,
1378 removed: !i.props.post_view.post.removed,
1379 reason: i.state.removeReason,
1382 WebSocketService.Instance.send(wsClient.removePost(form));
1384 i.state.showRemoveDialog = false;
1385 i.setState(i.state);
1388 handleModLock(i: PostListing) {
1389 let form: LockPost = {
1390 post_id: i.props.post_view.post.id,
1391 locked: !i.props.post_view.post.locked,
1394 WebSocketService.Instance.send(wsClient.lockPost(form));
1397 handleModSticky(i: PostListing) {
1398 let form: StickyPost = {
1399 post_id: i.props.post_view.post.id,
1400 stickied: !i.props.post_view.post.stickied,
1403 WebSocketService.Instance.send(wsClient.stickyPost(form));
1406 handleModBanFromCommunityShow(i: PostListing) {
1407 i.state.showBanDialog = true;
1408 i.state.banType = BanType.Community;
1409 i.setState(i.state);
1412 handleModBanShow(i: PostListing) {
1413 i.state.showBanDialog = true;
1414 i.state.banType = BanType.Site;
1415 i.setState(i.state);
1418 handleModBanReasonChange(i: PostListing, event: any) {
1419 i.state.banReason = event.target.value;
1420 i.setState(i.state);
1423 handleModBanExpiresChange(i: PostListing, event: any) {
1424 i.state.banExpires = event.target.value;
1425 i.setState(i.state);
1428 handleModBanFromCommunitySubmit(i: PostListing) {
1429 i.state.banType = BanType.Community;
1430 i.setState(i.state);
1431 i.handleModBanBothSubmit(i);
1434 handleModBanSubmit(i: PostListing) {
1435 i.state.banType = BanType.Site;
1436 i.setState(i.state);
1437 i.handleModBanBothSubmit(i);
1440 handleModBanBothSubmit(i: PostListing, event?: any) {
1441 if (event) event.preventDefault();
1443 if (i.state.banType == BanType.Community) {
1444 // If its an unban, restore all their data
1445 let ban = !i.props.post_view.creator_banned_from_community;
1447 i.state.removeData = false;
1449 let form: BanFromCommunity = {
1450 person_id: i.props.post_view.creator.id,
1451 community_id: i.props.post_view.community.id,
1453 remove_data: i.state.removeData,
1454 reason: i.state.banReason,
1455 expires: getUnixTime(i.state.banExpires),
1458 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1460 // If its an unban, restore all their data
1461 let ban = !i.props.post_view.creator.banned;
1463 i.state.removeData = false;
1465 let form: BanPerson = {
1466 person_id: i.props.post_view.creator.id,
1468 remove_data: i.state.removeData,
1469 reason: i.state.banReason,
1470 expires: getUnixTime(i.state.banExpires),
1473 WebSocketService.Instance.send(wsClient.banPerson(form));
1476 i.state.showBanDialog = false;
1477 i.setState(i.state);
1480 handleAddModToCommunity(i: PostListing) {
1481 let form: AddModToCommunity = {
1482 person_id: i.props.post_view.creator.id,
1483 community_id: i.props.post_view.community.id,
1487 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1488 i.setState(i.state);
1491 handleAddAdmin(i: PostListing) {
1492 let form: AddAdmin = {
1493 person_id: i.props.post_view.creator.id,
1497 WebSocketService.Instance.send(wsClient.addAdmin(form));
1498 i.setState(i.state);
1501 handleShowConfirmTransferCommunity(i: PostListing) {
1502 i.state.showConfirmTransferCommunity = true;
1503 i.setState(i.state);
1506 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1507 i.state.showConfirmTransferCommunity = false;
1508 i.setState(i.state);
1511 handleTransferCommunity(i: PostListing) {
1512 let form: TransferCommunity = {
1513 community_id: i.props.post_view.community.id,
1514 person_id: i.props.post_view.creator.id,
1517 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1518 i.state.showConfirmTransferCommunity = false;
1519 i.setState(i.state);
1522 handleShowConfirmTransferSite(i: PostListing) {
1523 i.state.showConfirmTransferSite = true;
1524 i.setState(i.state);
1527 handleCancelShowConfirmTransferSite(i: PostListing) {
1528 i.state.showConfirmTransferSite = false;
1529 i.setState(i.state);
1532 handleTransferSite(i: PostListing) {
1533 let form: TransferSite = {
1534 person_id: i.props.post_view.creator.id,
1537 WebSocketService.Instance.send(wsClient.transferSite(form));
1538 i.state.showConfirmTransferSite = false;
1539 i.setState(i.state);
1542 handleImageExpandClick(i: PostListing) {
1543 i.state.imageExpanded = !i.state.imageExpanded;
1544 i.setState(i.state);
1547 handleViewSource(i: PostListing) {
1548 i.state.viewSource = !i.state.viewSource;
1549 i.setState(i.state);
1552 handleShowAdvanced(i: PostListing) {
1553 i.state.showAdvanced = !i.state.showAdvanced;
1554 i.setState(i.state);
1558 handleShowMoreMobile(i: PostListing) {
1559 i.state.showMoreMobile = !i.state.showMoreMobile;
1560 i.state.showAdvanced = !i.state.showAdvanced;
1561 i.setState(i.state);
1565 handleShowBody(i: PostListing) {
1566 i.state.showBody = !i.state.showBody;
1567 i.setState(i.state);
1571 get pointsTippy(): string {
1572 let points = i18n.t("number_of_points", {
1573 count: this.state.score,
1576 let upvotes = i18n.t("number_of_upvotes", {
1577 count: this.state.upvotes,
1580 let downvotes = i18n.t("number_of_downvotes", {
1581 count: this.state.downvotes,
1584 return `${points} • ${upvotes} • ${downvotes}`;