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";
41 import { Icon } from "../common/icon";
42 import { MomentTime } from "../common/moment-time";
43 import { PictrsImage } from "../common/pictrs-image";
44 import { CommunityLink } from "../community/community-link";
45 import { PersonListing } from "../person/person-listing";
46 import { MetadataCard } from "./metadata-card";
47 import { PostForm } from "./post-form";
49 interface PostListingState {
51 showRemoveDialog: boolean;
53 showBanDialog: boolean;
58 showConfirmTransferSite: boolean;
59 showConfirmTransferCommunity: boolean;
60 imageExpanded: boolean;
62 showAdvanced: boolean;
63 showMoreMobile: boolean;
71 interface PostListingProps {
73 duplicates?: PostView[];
74 showCommunity?: boolean;
76 moderators?: CommunityModeratorView[];
77 admins?: PersonViewSafe[];
78 enableDownvotes: boolean;
82 export class PostListing extends Component<PostListingProps, PostListingState> {
83 private emptyState: PostListingState = {
85 showRemoveDialog: false,
91 banType: BanType.Community,
92 showConfirmTransferSite: false,
93 showConfirmTransferCommunity: false,
97 showMoreMobile: false,
99 my_vote: this.props.post_view.my_vote,
100 score: this.props.post_view.counts.score,
101 upvotes: this.props.post_view.counts.upvotes,
102 downvotes: this.props.post_view.counts.downvotes,
105 constructor(props: any, context: any) {
106 super(props, context);
108 this.state = this.emptyState;
109 this.handlePostLike = this.handlePostLike.bind(this);
110 this.handlePostDisLike = this.handlePostDisLike.bind(this);
111 this.handleEditPost = this.handleEditPost.bind(this);
112 this.handleEditCancel = this.handleEditCancel.bind(this);
115 componentWillReceiveProps(nextProps: PostListingProps) {
116 this.state.my_vote = nextProps.post_view.my_vote;
117 this.state.upvotes = nextProps.post_view.counts.upvotes;
118 this.state.downvotes = nextProps.post_view.counts.downvotes;
119 this.state.score = nextProps.post_view.counts.score;
120 if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
121 this.state.imageExpanded = false;
123 this.setState(this.state);
129 {!this.state.showEdit ? (
137 post_view={this.props.post_view}
138 onEdit={this.handleEditPost}
139 onCancel={this.handleEditCancel}
140 enableNsfw={this.props.enableNsfw}
141 enableDownvotes={this.props.enableDownvotes}
150 let post = this.props.post_view.post;
154 {post.url && this.showBody && post.embed_title && (
155 <MetadataCard post={post} />
159 (this.state.viewSource ? (
160 <pre>{post.body}</pre>
164 dangerouslySetInnerHTML={mdToHtml(post.body)}
172 imgThumb(src: string) {
173 let post_view = this.props.post_view;
179 nsfw={post_view.post.nsfw || post_view.community.nsfw}
184 getImageSrc(): string {
185 let post = this.props.post_view.post;
186 if (isImage(post.url)) {
187 if (post.url.includes("pictrs")) {
189 } else if (post.thumbnail_url) {
190 return post.thumbnail_url;
194 } else if (post.thumbnail_url) {
195 return post.thumbnail_url;
202 let post = this.props.post_view.post;
204 if (isImage(post.url)) {
207 class="float-right text-body pointer d-inline-block position-relative mb-2"
208 data-tippy-content={i18n.t("expand_here")}
209 onClick={linkEvent(this, this.handleImageExpandClick)}
211 aria-label={i18n.t("expand_here")}
213 {this.imgThumb(this.getImageSrc())}
214 <Icon icon="image" classes="mini-overlay" />
217 } else if (post.thumbnail_url) {
220 class="float-right text-body d-inline-block position-relative mb-2"
225 {this.imgThumb(this.getImageSrc())}
226 <Icon icon="external-link" classes="mini-overlay" />
229 } else if (post.url) {
230 if (isVideo(post.url)) {
232 <div class="embed-responsive embed-responsive-16by9">
238 class="embed-responsive-item"
240 <source src={post.url} type="video/mp4" />
247 className="text-body"
252 <div class="thumbnail rounded bg-light d-flex justify-content-center">
253 <Icon icon="external-link" classes="d-flex align-items-center" />
261 className="text-body"
262 to={`/post/${post.id}`}
263 title={i18n.t("comments")}
265 <div class="thumbnail rounded bg-light d-flex justify-content-center">
266 <Icon icon="message-square" classes="d-flex align-items-center" />
274 let post_view = this.props.post_view;
276 <ul class="list-inline mb-1 text-muted small">
277 <li className="list-inline-item">
278 <PersonListing person={post_view.creator} />
281 <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
284 <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
286 {(post_view.creator_banned_from_community ||
287 post_view.creator.banned) && (
288 <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
290 {post_view.creator_blocked && (
291 <span className="mx-1 badge badge-danger">{"blocked"}</span>
293 {this.props.showCommunity && (
295 <span class="mx-1"> {i18n.t("to")} </span>
296 <CommunityLink community={post_view.community} />
300 <li className="list-inline-item">•</li>
301 {post_view.post.url && !(hostname(post_view.post.url) == externalHost) && (
303 <li className="list-inline-item">
305 className="text-muted font-italic"
306 href={post_view.post.url}
307 title={post_view.post.url}
310 {hostname(post_view.post.url)}
313 <li className="list-inline-item">•</li>
316 <li className="list-inline-item">
318 <MomentTime data={post_view.post} />
321 {post_view.post.body && (
323 <li className="list-inline-item">•</li>
324 <li className="list-inline-item">
326 className="text-muted btn btn-sm btn-link p-0"
327 data-tippy-content={md.render(
328 previewLines(post_view.post.body)
330 data-tippy-allowHtml={true}
331 onClick={linkEvent(this, this.handleShowBody)}
333 <Icon icon="book-open" classes="icon-inline mr-1" />
344 <div className={`vote-bar col-1 pr-0 small text-center`}>
346 className={`btn-animate btn btn-link p-0 ${
347 this.state.my_vote == 1 ? "text-info" : "text-muted"
349 onClick={linkEvent(this, this.handlePostLike)}
350 data-tippy-content={i18n.t("upvote")}
351 aria-label={i18n.t("upvote")}
353 <Icon icon="arrow-up1" classes="upvote" />
357 class={`unselectable pointer font-weight-bold text-muted px-1`}
358 data-tippy-content={this.pointsTippy}
360 {numToSI(this.state.score)}
363 <div class="p-1"></div>
365 {this.props.enableDownvotes && (
367 className={`btn-animate btn btn-link p-0 ${
368 this.state.my_vote == -1 ? "text-danger" : "text-muted"
370 onClick={linkEvent(this, this.handlePostDisLike)}
371 data-tippy-content={i18n.t("downvote")}
372 aria-label={i18n.t("downvote")}
374 <Icon icon="arrow-down1" classes="downvote" />
382 let post = this.props.post_view.post;
384 <div className="post-title overflow-hidden">
386 {this.showBody && post.url ? (
388 className={!post.stickied ? "text-body" : "text-primary"}
397 className={!post.stickied ? "text-body" : "text-primary"}
398 to={`/post/${post.id}`}
399 title={i18n.t("comments")}
404 {(isImage(post.url) || post.thumbnail_url) &&
405 (!this.state.imageExpanded ? (
407 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
408 data-tippy-content={i18n.t("expand_here")}
409 onClick={linkEvent(this, this.handleImageExpandClick)}
411 <Icon icon="plus-square" classes="icon-inline" />
416 class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
417 onClick={linkEvent(this, this.handleImageExpandClick)}
419 <Icon icon="minus-square" classes="icon-inline" />
423 class="btn btn-link d-inline-block"
424 onClick={linkEvent(this, this.handleImageExpandClick)}
426 <PictrsImage src={this.getImageSrc()} />
432 <small className="ml-2 text-muted font-italic">
438 className="unselectable pointer ml-2 text-muted font-italic"
439 data-tippy-content={i18n.t("deleted")}
441 <Icon icon="trash" classes="icon-inline text-danger" />
446 className="unselectable pointer ml-2 text-muted font-italic"
447 data-tippy-content={i18n.t("locked")}
449 <Icon icon="lock" classes="icon-inline text-danger" />
454 className="unselectable pointer ml-2 text-muted font-italic"
455 data-tippy-content={i18n.t("stickied")}
457 <Icon icon="pin" classes="icon-inline text-primary" />
461 <small className="ml-2 text-muted font-italic">
470 commentsLine(mobile = false) {
471 let post_view = this.props.post_view;
473 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
474 <button class="btn btn-link text-muted p-0">
476 className="text-muted small"
477 title={i18n.t("number_of_comments", {
478 count: post_view.counts.comments,
479 formattedCount: post_view.counts.comments,
481 to={`/post/${post_view.post.id}?scrollToComments=true`}
483 <Icon icon="message-square" classes="icon-inline mr-1" />
484 {i18n.t("number_of_comments", {
485 count: post_view.counts.comments,
486 formattedCount: numToSI(post_view.counts.comments),
492 {this.state.downvotes !== 0 && showScores() && (
494 class="btn text-muted py-0 pr-0"
495 data-tippy-content={this.pointsTippy}
496 aria-label={i18n.t("downvote")}
499 <Icon icon="arrow-down1" classes="icon-inline mr-1" />
500 <span>{numToSI(this.state.downvotes)}</span>
506 class="btn btn-link btn-animate text-muted py-0"
507 onClick={linkEvent(this, this.handleSavePostClick)}
509 post_view.saved ? i18n.t("unsave") : i18n.t("save")
511 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
516 classes={`icon-inline ${post_view.saved && "text-warning"}`}
523 {/* This is an expanding spacer for mobile */}
524 <div className="flex-grow-1"></div>
530 className={`btn-animate btn py-0 px-1 ${
531 this.state.my_vote == 1 ? "text-info" : "text-muted"
533 data-tippy-content={this.pointsTippy}
534 onClick={linkEvent(this, this.handlePostLike)}
535 aria-label={i18n.t("upvote")}
537 <Icon icon="arrow-up1" classes="icon-inline small mr-2" />
538 {numToSI(this.state.upvotes)}
542 className={`btn-animate btn py-0 px-1 ${
543 this.state.my_vote == 1 ? "text-info" : "text-muted"
545 onClick={linkEvent(this, this.handlePostLike)}
546 aria-label={i18n.t("upvote")}
548 <Icon icon="arrow-up1" classes="icon-inline small" />
551 {this.props.enableDownvotes &&
554 className={`ml-2 btn-animate btn py-0 pl-1 ${
555 this.state.my_vote == -1 ? "text-danger" : "text-muted"
557 onClick={linkEvent(this, this.handlePostDisLike)}
558 data-tippy-content={this.pointsTippy}
559 aria-label={i18n.t("downvote")}
561 <Icon icon="arrow-down1" classes="icon-inline small mr-2" />
562 {this.state.downvotes !== 0 && (
563 <span>{numToSI(this.state.downvotes)}</span>
568 className={`ml-2 btn-animate btn py-0 pl-1 ${
569 this.state.my_vote == -1 ? "text-danger" : "text-muted"
571 onClick={linkEvent(this, this.handlePostDisLike)}
572 aria-label={i18n.t("downvote")}
574 <Icon icon="arrow-down1" classes="icon-inline small" />
579 class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
580 onClick={linkEvent(this, this.handleSavePostClick)}
581 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
583 post_view.saved ? i18n.t("unsave") : i18n.t("save")
588 classes={`icon-inline ${post_view.saved && "text-warning"}`}
592 {!this.state.showMoreMobile && this.showBody && (
594 class="btn btn-link btn-animate text-muted py-0"
595 onClick={linkEvent(this, this.handleShowMoreMobile)}
596 aria-label={i18n.t("more")}
597 data-tippy-content={i18n.t("more")}
599 <Icon icon="more-vertical" classes="icon-inline" />
602 {this.state.showMoreMobile && this.postActions(mobile)}
610 let dupes = this.props.duplicates;
613 dupes.length > 0 && (
614 <ul class="list-inline mb-1 small text-muted">
616 <li className="list-inline-item mr-2">
617 {i18n.t("cross_posted_to")}
620 <li className="list-inline-item mr-2">
621 <Link to={`/post/${pv.post.id}`}>
624 : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
634 postActions(mobile = false) {
635 let post_view = this.props.post_view;
637 UserService.Instance.myUserInfo && (
643 class="btn btn-link btn-animate text-muted py-0 pl-0"
644 onClick={linkEvent(this, this.handleSavePostClick)}
646 post_view.saved ? i18n.t("unsave") : i18n.t("save")
649 post_view.saved ? i18n.t("unsave") : i18n.t("save")
654 classes={`icon-inline ${post_view.saved && "text-warning"}`}
659 className="btn btn-link btn-animate text-muted py-0"
660 to={`/create_post${this.crossPostParams}`}
661 title={i18n.t("cross_post")}
663 <Icon icon="copy" classes="icon-inline" />
667 class="btn btn-link btn-animate text-muted py-0"
668 onClick={linkEvent(this, this.handleBlockUserClick)}
669 data-tippy-content={i18n.t("block_user")}
670 aria-label={i18n.t("block_user")}
672 <Icon icon="slash" classes="icon-inline" />
677 {this.myPost && this.showBody && (
680 class="btn btn-link btn-animate text-muted py-0"
681 onClick={linkEvent(this, this.handleEditClick)}
682 data-tippy-content={i18n.t("edit")}
683 aria-label={i18n.t("edit")}
685 <Icon icon="edit" classes="icon-inline" />
688 class="btn btn-link btn-animate text-muted py-0"
689 onClick={linkEvent(this, this.handleDeleteClick)}
691 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
694 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
699 classes={`icon-inline ${
700 post_view.post.deleted && "text-danger"
707 {!this.state.showAdvanced && this.showBody ? (
709 class="btn btn-link btn-animate text-muted py-0"
710 onClick={linkEvent(this, this.handleShowAdvanced)}
711 data-tippy-content={i18n.t("more")}
712 aria-label={i18n.t("more")}
714 <Icon icon="more-vertical" classes="icon-inline" />
718 {this.showBody && post_view.post.body && (
720 class="btn btn-link btn-animate text-muted py-0"
721 onClick={linkEvent(this, this.handleViewSource)}
722 data-tippy-content={i18n.t("view_source")}
723 aria-label={i18n.t("view_source")}
727 classes={`icon-inline ${
728 this.state.viewSource && "text-success"
733 {this.canModOnSelf && (
736 class="btn btn-link btn-animate text-muted py-0"
737 onClick={linkEvent(this, this.handleModLock)}
739 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
742 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
747 classes={`icon-inline ${
748 post_view.post.locked && "text-danger"
753 class="btn btn-link btn-animate text-muted py-0"
754 onClick={linkEvent(this, this.handleModSticky)}
756 post_view.post.stickied
761 post_view.post.stickied
768 classes={`icon-inline ${
769 post_view.post.stickied && "text-success"
775 {/* Mods can ban from community, and appoint as mods to community */}
776 {(this.canMod || this.canAdmin) &&
777 (!post_view.post.removed ? (
779 class="btn btn-link btn-animate text-muted py-0"
780 onClick={linkEvent(this, this.handleModRemoveShow)}
781 aria-label={i18n.t("remove")}
787 class="btn btn-link btn-animate text-muted py-0"
788 onClick={linkEvent(this, this.handleModRemoveSubmit)}
789 aria-label={i18n.t("restore")}
797 (!post_view.creator_banned_from_community ? (
799 class="btn btn-link btn-animate text-muted py-0"
802 this.handleModBanFromCommunityShow
804 aria-label={i18n.t("ban")}
810 class="btn btn-link btn-animate text-muted py-0"
813 this.handleModBanFromCommunitySubmit
815 aria-label={i18n.t("unban")}
820 {!post_view.creator_banned_from_community && (
822 class="btn btn-link btn-animate text-muted py-0"
823 onClick={linkEvent(this, this.handleAddModToCommunity)}
826 ? i18n.t("remove_as_mod")
827 : i18n.t("appoint_as_mod")
831 ? i18n.t("remove_as_mod")
832 : i18n.t("appoint_as_mod")}
837 {/* Community creators and admins can transfer community to another mod */}
838 {(this.amCommunityCreator || this.canAdmin) &&
840 (!this.state.showConfirmTransferCommunity ? (
842 class="btn btn-link btn-animate text-muted py-0"
845 this.handleShowConfirmTransferCommunity
847 aria-label={i18n.t("transfer_community")}
849 {i18n.t("transfer_community")}
854 class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
855 aria-label={i18n.t("are_you_sure")}
857 {i18n.t("are_you_sure")}
860 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
861 aria-label={i18n.t("yes")}
862 onClick={linkEvent(this, this.handleTransferCommunity)}
867 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
870 this.handleCancelShowConfirmTransferCommunity
872 aria-label={i18n.t("no")}
878 {/* Admins can ban from all, and appoint other admins */}
882 (!post_view.creator.banned ? (
884 class="btn btn-link btn-animate text-muted py-0"
885 onClick={linkEvent(this, this.handleModBanShow)}
886 aria-label={i18n.t("ban_from_site")}
888 {i18n.t("ban_from_site")}
892 class="btn btn-link btn-animate text-muted py-0"
893 onClick={linkEvent(this, this.handleModBanSubmit)}
894 aria-label={i18n.t("unban_from_site")}
896 {i18n.t("unban_from_site")}
899 {!post_view.creator.banned && post_view.creator.local && (
901 class="btn btn-link btn-animate text-muted py-0"
902 onClick={linkEvent(this, this.handleAddAdmin)}
905 ? i18n.t("remove_as_admin")
906 : i18n.t("appoint_as_admin")
910 ? i18n.t("remove_as_admin")
911 : i18n.t("appoint_as_admin")}
916 {/* Site Creator can transfer to another admin */}
917 {this.amSiteCreator &&
919 (!this.state.showConfirmTransferSite ? (
921 class="btn btn-link btn-animate text-muted py-0"
924 this.handleShowConfirmTransferSite
926 aria-label={i18n.t("transfer_site")}
928 {i18n.t("transfer_site")}
933 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
934 aria-label={i18n.t("are_you_sure")}
936 {i18n.t("are_you_sure")}
939 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
940 onClick={linkEvent(this, this.handleTransferSite)}
941 aria-label={i18n.t("yes")}
946 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
949 this.handleCancelShowConfirmTransferSite
951 aria-label={i18n.t("no")}
964 removeAndBanDialogs() {
965 let post = this.props.post_view;
968 {this.state.showRemoveDialog && (
971 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
973 <label class="sr-only" htmlFor="post-listing-remove-reason">
978 id="post-listing-remove-reason"
979 class="form-control mr-2"
980 placeholder={i18n.t("reason")}
981 value={this.state.removeReason}
982 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
986 class="btn btn-secondary"
987 aria-label={i18n.t("remove_post")}
989 {i18n.t("remove_post")}
993 {this.state.showBanDialog && (
994 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
995 <div class="form-group row">
996 <label class="col-form-label" htmlFor="post-listing-ban-reason">
1001 id="post-listing-ban-reason"
1002 class="form-control mr-2"
1003 placeholder={i18n.t("reason")}
1004 value={this.state.banReason}
1005 onInput={linkEvent(this, this.handleModBanReasonChange)}
1007 <div class="form-group">
1008 <div class="form-check">
1010 class="form-check-input"
1011 id="mod-ban-remove-data"
1013 checked={this.state.removeData}
1014 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1017 class="form-check-label"
1018 htmlFor="mod-ban-remove-data"
1019 title={i18n.t("remove_content_more")}
1021 {i18n.t("remove_content")}
1026 {/* TODO hold off on expires until later */}
1027 {/* <div class="form-group row"> */}
1028 {/* <label class="col-form-label">Expires</label> */}
1029 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1031 <div class="form-group row">
1034 class="btn btn-secondary"
1035 aria-label={i18n.t("ban")}
1037 {i18n.t("ban")} {post.creator.name}
1047 let post = this.props.post_view.post;
1048 return post.thumbnail_url || isImage(post.url) ? (
1050 <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1051 {this.postTitleLine()}
1054 {/* Post body prev or thumbnail */}
1055 {!this.state.imageExpanded && this.thumbnail()}
1059 this.postTitleLine()
1063 showMobilePreview() {
1064 let post = this.props.post_view.post;
1069 className="md-div mb-1"
1070 dangerouslySetInnerHTML={{
1071 __html: md.render(previewLines(post.body)),
1081 {/* The mobile view*/}
1082 <div class="d-block d-sm-none">
1084 <div class="col-12">
1085 {this.createdLine()}
1087 {/* If it has a thumbnail, do a right aligned thumbnail */}
1088 {this.mobileThumbnail()}
1090 {/* Show a preview of the post body */}
1091 {this.showMobilePreview()}
1093 {this.commentsLine(true)}
1094 {this.duplicatesLine()}
1095 {this.removeAndBanDialogs()}
1100 {/* The larger view*/}
1101 <div class="d-none d-sm-block">
1104 {!this.state.imageExpanded && (
1105 <div class="col-sm-2 pr-0">
1106 <div class="">{this.thumbnail()}</div>
1111 this.state.imageExpanded ? "col-12" : "col-12 col-sm-9"
1115 <div className="col-12">
1116 {this.postTitleLine()}
1117 {this.createdLine()}
1118 {this.commentsLine()}
1119 {this.duplicatesLine()}
1120 {this.postActions()}
1121 {this.removeAndBanDialogs()}
1131 private get myPost(): boolean {
1133 UserService.Instance.myUserInfo &&
1134 this.props.post_view.creator.id ==
1135 UserService.Instance.myUserInfo.local_user_view.person.id
1139 get isMod(): boolean {
1141 this.props.moderators &&
1143 this.props.moderators.map(m => m.moderator.id),
1144 this.props.post_view.creator.id
1149 get isAdmin(): boolean {
1151 this.props.admins &&
1153 this.props.admins.map(a => a.person.id),
1154 this.props.post_view.creator.id
1159 get canMod(): boolean {
1160 if (this.props.admins && this.props.moderators) {
1161 let adminsThenMods = this.props.admins
1162 .map(a => a.person.id)
1163 .concat(this.props.moderators.map(m => m.moderator.id));
1166 UserService.Instance.myUserInfo,
1168 this.props.post_view.creator.id
1175 get canModOnSelf(): boolean {
1176 if (this.props.admins && this.props.moderators) {
1177 let adminsThenMods = this.props.admins
1178 .map(a => a.person.id)
1179 .concat(this.props.moderators.map(m => m.moderator.id));
1182 UserService.Instance.myUserInfo,
1184 this.props.post_view.creator.id,
1192 get canAdmin(): boolean {
1194 this.props.admins &&
1196 UserService.Instance.myUserInfo,
1197 this.props.admins.map(a => a.person.id),
1198 this.props.post_view.creator.id
1203 get amCommunityCreator(): boolean {
1205 this.props.moderators &&
1206 UserService.Instance.myUserInfo &&
1207 this.props.post_view.creator.id !=
1208 UserService.Instance.myUserInfo.local_user_view.person.id &&
1209 UserService.Instance.myUserInfo.local_user_view.person.id ==
1210 this.props.moderators[0].moderator.id
1214 get amSiteCreator(): boolean {
1216 this.props.admins &&
1217 UserService.Instance.myUserInfo &&
1218 this.props.post_view.creator.id !=
1219 UserService.Instance.myUserInfo.local_user_view.person.id &&
1220 UserService.Instance.myUserInfo.local_user_view.person.id ==
1221 this.props.admins[0].person.id
1225 handlePostLike(i: PostListing, event: any) {
1226 event.preventDefault();
1227 if (!UserService.Instance.myUserInfo) {
1228 this.context.router.history.push(`/login`);
1231 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1233 if (i.state.my_vote == 1) {
1236 } else if (i.state.my_vote == -1) {
1237 i.state.downvotes--;
1245 i.state.my_vote = new_vote;
1247 let form: CreatePostLike = {
1248 post_id: i.props.post_view.post.id,
1249 score: i.state.my_vote,
1253 WebSocketService.Instance.send(wsClient.likePost(form));
1254 i.setState(i.state);
1258 handlePostDisLike(i: PostListing, event: any) {
1259 event.preventDefault();
1260 if (!UserService.Instance.myUserInfo) {
1261 this.context.router.history.push(`/login`);
1264 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1266 if (i.state.my_vote == 1) {
1269 i.state.downvotes++;
1270 } else if (i.state.my_vote == -1) {
1271 i.state.downvotes--;
1274 i.state.downvotes++;
1278 i.state.my_vote = new_vote;
1280 let form: CreatePostLike = {
1281 post_id: i.props.post_view.post.id,
1282 score: i.state.my_vote,
1286 WebSocketService.Instance.send(wsClient.likePost(form));
1287 i.setState(i.state);
1291 handleEditClick(i: PostListing) {
1292 i.state.showEdit = true;
1293 i.setState(i.state);
1296 handleEditCancel() {
1297 this.state.showEdit = false;
1298 this.setState(this.state);
1301 // The actual editing is done in the recieve for post
1303 this.state.showEdit = false;
1304 this.setState(this.state);
1307 handleBlockUserClick(i: PostListing) {
1308 let blockUserForm: BlockPerson = {
1309 person_id: i.props.post_view.creator.id,
1313 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1316 handleDeleteClick(i: PostListing) {
1317 let deleteForm: DeletePost = {
1318 post_id: i.props.post_view.post.id,
1319 deleted: !i.props.post_view.post.deleted,
1322 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1325 handleSavePostClick(i: PostListing) {
1327 i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1328 let form: SavePost = {
1329 post_id: i.props.post_view.post.id,
1334 WebSocketService.Instance.send(wsClient.savePost(form));
1337 get crossPostParams(): string {
1338 let post = this.props.post_view.post;
1339 let params = `?title=${encodeURIComponent(post.name)}`;
1342 params += `&url=${encodeURIComponent(post.url)}`;
1345 params += `&body=${encodeURIComponent(this.crossPostBody())}`;
1350 crossPostBody(): string {
1351 let post = this.props.post_view.post;
1352 let body = `${i18n.t("cross_posted_from")} ${
1354 }\n\n${post.body.replace(/^/gm, "> ")}`;
1358 get showBody(): boolean {
1359 return this.props.showBody || this.state.showBody;
1362 handleModRemoveShow(i: PostListing) {
1363 i.state.showRemoveDialog = true;
1364 i.setState(i.state);
1367 handleModRemoveReasonChange(i: PostListing, event: any) {
1368 i.state.removeReason = event.target.value;
1369 i.setState(i.state);
1372 handleModRemoveDataChange(i: PostListing, event: any) {
1373 i.state.removeData = event.target.checked;
1374 i.setState(i.state);
1377 handleModRemoveSubmit(i: PostListing, event: any) {
1378 event.preventDefault();
1379 let form: RemovePost = {
1380 post_id: i.props.post_view.post.id,
1381 removed: !i.props.post_view.post.removed,
1382 reason: i.state.removeReason,
1385 WebSocketService.Instance.send(wsClient.removePost(form));
1387 i.state.showRemoveDialog = false;
1388 i.setState(i.state);
1391 handleModLock(i: PostListing) {
1392 let form: LockPost = {
1393 post_id: i.props.post_view.post.id,
1394 locked: !i.props.post_view.post.locked,
1397 WebSocketService.Instance.send(wsClient.lockPost(form));
1400 handleModSticky(i: PostListing) {
1401 let form: StickyPost = {
1402 post_id: i.props.post_view.post.id,
1403 stickied: !i.props.post_view.post.stickied,
1406 WebSocketService.Instance.send(wsClient.stickyPost(form));
1409 handleModBanFromCommunityShow(i: PostListing) {
1410 i.state.showBanDialog = true;
1411 i.state.banType = BanType.Community;
1412 i.setState(i.state);
1415 handleModBanShow(i: PostListing) {
1416 i.state.showBanDialog = true;
1417 i.state.banType = BanType.Site;
1418 i.setState(i.state);
1421 handleModBanReasonChange(i: PostListing, event: any) {
1422 i.state.banReason = event.target.value;
1423 i.setState(i.state);
1426 handleModBanExpiresChange(i: PostListing, event: any) {
1427 i.state.banExpires = event.target.value;
1428 i.setState(i.state);
1431 handleModBanFromCommunitySubmit(i: PostListing) {
1432 i.state.banType = BanType.Community;
1433 i.setState(i.state);
1434 i.handleModBanBothSubmit(i);
1437 handleModBanSubmit(i: PostListing) {
1438 i.state.banType = BanType.Site;
1439 i.setState(i.state);
1440 i.handleModBanBothSubmit(i);
1443 handleModBanBothSubmit(i: PostListing, event?: any) {
1444 if (event) event.preventDefault();
1446 if (i.state.banType == BanType.Community) {
1447 // If its an unban, restore all their data
1448 let ban = !i.props.post_view.creator_banned_from_community;
1450 i.state.removeData = false;
1452 let form: BanFromCommunity = {
1453 person_id: i.props.post_view.creator.id,
1454 community_id: i.props.post_view.community.id,
1456 remove_data: i.state.removeData,
1457 reason: i.state.banReason,
1458 expires: getUnixTime(i.state.banExpires),
1461 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1463 // If its an unban, restore all their data
1464 let ban = !i.props.post_view.creator.banned;
1466 i.state.removeData = false;
1468 let form: BanPerson = {
1469 person_id: i.props.post_view.creator.id,
1471 remove_data: i.state.removeData,
1472 reason: i.state.banReason,
1473 expires: getUnixTime(i.state.banExpires),
1476 WebSocketService.Instance.send(wsClient.banPerson(form));
1479 i.state.showBanDialog = false;
1480 i.setState(i.state);
1483 handleAddModToCommunity(i: PostListing) {
1484 let form: AddModToCommunity = {
1485 person_id: i.props.post_view.creator.id,
1486 community_id: i.props.post_view.community.id,
1490 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1491 i.setState(i.state);
1494 handleAddAdmin(i: PostListing) {
1495 let form: AddAdmin = {
1496 person_id: i.props.post_view.creator.id,
1500 WebSocketService.Instance.send(wsClient.addAdmin(form));
1501 i.setState(i.state);
1504 handleShowConfirmTransferCommunity(i: PostListing) {
1505 i.state.showConfirmTransferCommunity = true;
1506 i.setState(i.state);
1509 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1510 i.state.showConfirmTransferCommunity = false;
1511 i.setState(i.state);
1514 handleTransferCommunity(i: PostListing) {
1515 let form: TransferCommunity = {
1516 community_id: i.props.post_view.community.id,
1517 person_id: i.props.post_view.creator.id,
1520 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1521 i.state.showConfirmTransferCommunity = false;
1522 i.setState(i.state);
1525 handleShowConfirmTransferSite(i: PostListing) {
1526 i.state.showConfirmTransferSite = true;
1527 i.setState(i.state);
1530 handleCancelShowConfirmTransferSite(i: PostListing) {
1531 i.state.showConfirmTransferSite = false;
1532 i.setState(i.state);
1535 handleTransferSite(i: PostListing) {
1536 let form: TransferSite = {
1537 person_id: i.props.post_view.creator.id,
1540 WebSocketService.Instance.send(wsClient.transferSite(form));
1541 i.state.showConfirmTransferSite = false;
1542 i.setState(i.state);
1545 handleImageExpandClick(i: PostListing) {
1546 i.state.imageExpanded = !i.state.imageExpanded;
1547 i.setState(i.state);
1550 handleViewSource(i: PostListing) {
1551 i.state.viewSource = !i.state.viewSource;
1552 i.setState(i.state);
1555 handleShowAdvanced(i: PostListing) {
1556 i.state.showAdvanced = !i.state.showAdvanced;
1557 i.setState(i.state);
1561 handleShowMoreMobile(i: PostListing) {
1562 i.state.showMoreMobile = !i.state.showMoreMobile;
1563 i.state.showAdvanced = !i.state.showAdvanced;
1564 i.setState(i.state);
1568 handleShowBody(i: PostListing) {
1569 i.state.showBody = !i.state.showBody;
1570 i.setState(i.state);
1574 get pointsTippy(): string {
1575 let points = i18n.t("number_of_points", {
1576 count: this.state.score,
1577 formattedCount: this.state.score,
1580 let upvotes = i18n.t("number_of_upvotes", {
1581 count: this.state.upvotes,
1582 formattedCount: this.state.upvotes,
1585 let downvotes = i18n.t("number_of_downvotes", {
1586 count: this.state.downvotes,
1587 formattedCount: this.state.downvotes,
1590 return `${points} • ${upvotes} • ${downvotes}`;