1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
3 import { WebSocketService, UserService } from "../services";
19 CommunityModeratorView,
20 } from "lemmy-js-client";
21 import { BanType } from "../interfaces";
22 import { MomentTime } from "./moment-time";
23 import { PostForm } from "./post-form";
24 import { IFramelyCard } from "./iframely-card";
25 import { PersonListing } from "./person-listing";
26 import { CommunityLink } from "./community-link";
27 import { PictrsImage } from "./pictrs-image";
28 import { Icon } from "./icon";
43 import { i18n } from "../i18next";
44 import { externalHost } from "../env";
46 interface PostListingState {
48 showRemoveDialog: boolean;
50 showBanDialog: boolean;
55 showConfirmTransferSite: boolean;
56 showConfirmTransferCommunity: boolean;
57 imageExpanded: boolean;
59 showAdvanced: boolean;
60 showMoreMobile: boolean;
67 interface PostListingProps {
69 duplicates?: PostView[];
70 showCommunity?: boolean;
72 moderators?: CommunityModeratorView[];
73 admins?: PersonViewSafe[];
74 enableDownvotes: boolean;
78 export class PostListing extends Component<PostListingProps, PostListingState> {
79 private emptyState: PostListingState = {
81 showRemoveDialog: false,
87 banType: BanType.Community,
88 showConfirmTransferSite: false,
89 showConfirmTransferCommunity: false,
93 showMoreMobile: false,
94 my_vote: this.props.post_view.my_vote,
95 score: this.props.post_view.counts.score,
96 upvotes: this.props.post_view.counts.upvotes,
97 downvotes: this.props.post_view.counts.downvotes,
100 constructor(props: any, context: any) {
101 super(props, context);
103 this.state = this.emptyState;
104 this.handlePostLike = this.handlePostLike.bind(this);
105 this.handlePostDisLike = this.handlePostDisLike.bind(this);
106 this.handleEditPost = this.handleEditPost.bind(this);
107 this.handleEditCancel = this.handleEditCancel.bind(this);
110 componentWillReceiveProps(nextProps: PostListingProps) {
111 this.state.my_vote = nextProps.post_view.my_vote;
112 this.state.upvotes = nextProps.post_view.counts.upvotes;
113 this.state.downvotes = nextProps.post_view.counts.downvotes;
114 this.state.score = nextProps.post_view.counts.score;
115 if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
116 this.state.imageExpanded = false;
118 this.setState(this.state);
124 {!this.state.showEdit ? (
132 post_view={this.props.post_view}
133 onEdit={this.handleEditPost}
134 onCancel={this.handleEditCancel}
135 enableNsfw={this.props.enableNsfw}
136 enableDownvotes={this.props.enableDownvotes}
145 let post = this.props.post_view.post;
149 {post.url && this.props.showBody && post.embed_title && (
150 <IFramelyCard post={post} />
152 {this.props.showBody &&
154 (this.state.viewSource ? (
155 <pre>{post.body}</pre>
159 dangerouslySetInnerHTML={mdToHtml(post.body)}
167 imgThumb(src: string) {
168 let post_view = this.props.post_view;
174 nsfw={post_view.post.nsfw || post_view.community.nsfw}
179 getImageSrc(): string {
180 let post = this.props.post_view.post;
181 if (isImage(post.url)) {
182 if (post.url.includes("pictrs")) {
184 } else if (post.thumbnail_url) {
185 return post.thumbnail_url;
189 } else if (post.thumbnail_url) {
190 return post.thumbnail_url;
197 let post = this.props.post_view.post;
199 if (isImage(post.url)) {
202 class="float-right text-body pointer d-inline-block position-relative mb-2"
203 data-tippy-content={i18n.t("expand_here")}
204 onClick={linkEvent(this, this.handleImageExpandClick)}
206 aria-label={i18n.t("expand_here")}
208 {this.imgThumb(this.getImageSrc())}
209 <Icon icon="image" classes="mini-overlay" />
212 } else if (post.thumbnail_url) {
215 class="float-right text-body d-inline-block position-relative mb-2"
220 {this.imgThumb(this.getImageSrc())}
221 <Icon icon="external-link" classes="mini-overlay" />
224 } else if (post.url) {
225 if (isVideo(post.url)) {
227 <div class="embed-responsive embed-responsive-16by9">
233 class="embed-responsive-item"
235 <source src={post.url} type="video/mp4" />
242 className="text-body"
247 <div class="thumbnail rounded bg-light d-flex justify-content-center">
248 <Icon icon="external-link" classes="d-flex align-items-center" />
256 className="text-body"
257 to={`/post/${post.id}`}
258 title={i18n.t("comments")}
260 <div class="thumbnail rounded bg-light d-flex justify-content-center">
261 <Icon icon="message-square" classes="d-flex align-items-center" />
269 let post_view = this.props.post_view;
271 <ul class="list-inline mb-1 text-muted small">
272 <li className="list-inline-item">
273 <PersonListing person={post_view.creator} />
276 <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
279 <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
281 {(post_view.creator_banned_from_community ||
282 post_view.creator.banned) && (
283 <span className="mx-1 badge badge-danger">{i18n.t("banned")}</span>
285 {this.props.showCommunity && (
287 <span class="mx-1"> {i18n.t("to")} </span>
288 <CommunityLink community={post_view.community} />
292 <li className="list-inline-item">•</li>
293 {post_view.post.url && !(hostname(post_view.post.url) == externalHost) && (
295 <li className="list-inline-item">
297 className="text-muted font-italic"
298 href={post_view.post.url}
299 title={post_view.post.url}
302 {hostname(post_view.post.url)}
305 <li className="list-inline-item">•</li>
308 <li className="list-inline-item">
310 <MomentTime data={post_view.post} />
313 {post_view.post.body && (
315 <li className="list-inline-item">•</li>
316 <li className="list-inline-item">
317 {/* Using a link with tippy doesn't work on touch devices unfortunately */}
319 className="text-muted"
320 data-tippy-content={md.render(
321 previewLines(post_view.post.body)
323 data-tippy-allowHtml={true}
324 aria-label={i18n.t("upvote")}
325 to={`/post/${post_view.post.id}`}
327 <Icon icon="book-open" classes="icon-inline mr-1" />
338 <div className={`vote-bar col-1 pr-0 small text-center`}>
340 className={`btn-animate btn btn-link p-0 ${
341 this.state.my_vote == 1 ? "text-info" : "text-muted"
343 onClick={linkEvent(this, this.handlePostLike)}
344 data-tippy-content={i18n.t("upvote")}
345 aria-label={i18n.t("upvote")}
347 <Icon icon="arrow-up1" classes="upvote" />
350 class={`unselectable pointer font-weight-bold text-muted px-1`}
351 data-tippy-content={this.pointsTippy}
355 {this.props.enableDownvotes && (
357 className={`btn-animate btn btn-link p-0 ${
358 this.state.my_vote == -1 ? "text-danger" : "text-muted"
360 onClick={linkEvent(this, this.handlePostDisLike)}
361 data-tippy-content={i18n.t("downvote")}
362 aria-label={i18n.t("downvote")}
364 <Icon icon="arrow-down1" classes="downvote" />
372 let post = this.props.post_view.post;
374 <div className="post-title overflow-hidden">
376 {this.props.showBody && post.url ? (
378 className={!post.stickied ? "text-body" : "text-primary"}
387 className={!post.stickied ? "text-body" : "text-primary"}
388 to={`/post/${post.id}`}
389 title={i18n.t("comments")}
394 {(isImage(post.url) || post.thumbnail_url) &&
395 (!this.state.imageExpanded ? (
397 class="text-monospace unselectable pointer ml-2 text-muted small"
398 data-tippy-content={i18n.t("expand_here")}
399 onClick={linkEvent(this, this.handleImageExpandClick)}
401 <Icon icon="plus-square" classes="icon-inline" />
406 class="text-monospace unselectable pointer ml-2 text-muted small"
407 onClick={linkEvent(this, this.handleImageExpandClick)}
409 <Icon icon="minus-square" classes="icon-inline" />
414 onClick={linkEvent(this, this.handleImageExpandClick)}
416 <PictrsImage src={this.getImageSrc()} />
422 <small className="ml-2 text-muted font-italic">
428 className="unselectable pointer ml-2 text-muted font-italic"
429 data-tippy-content={i18n.t("deleted")}
431 <Icon icon="trash" classes="icon-inline text-danger" />
436 className="unselectable pointer ml-2 text-muted font-italic"
437 data-tippy-content={i18n.t("locked")}
439 <Icon icon="lock" classes="icon-inline text-danger" />
444 className="unselectable pointer ml-2 text-muted font-italic"
445 data-tippy-content={i18n.t("stickied")}
447 <Icon icon="pin" classes="icon-inline text-primary" />
451 <small className="ml-2 text-muted font-italic">
460 commentsLine(mobile = false) {
461 let post_view = this.props.post_view;
463 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
464 <button class="btn btn-link text-muted p-0">
466 className="text-muted small"
467 title={i18n.t("number_of_comments", {
468 count: post_view.counts.comments,
470 to={`/post/${post_view.post.id}`}
472 <Icon icon="message-square" classes="icon-inline mr-1" />
473 {i18n.t("number_of_comments", {
474 count: post_view.counts.comments,
480 {this.state.downvotes !== 0 && (
482 class="btn text-muted py-0 pr-0"
483 data-tippy-content={this.pointsTippy}
484 aria-label={i18n.t("downvote")}
487 <Icon icon="arrow-down1" classes="icon-inline mr-1" />
488 <span>{this.state.downvotes}</span>
492 {!this.props.showBody && (
494 class="btn btn-link btn-animate text-muted py-0"
495 onClick={linkEvent(this, this.handleSavePostClick)}
497 post_view.saved ? i18n.t("unsave") : i18n.t("save")
499 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
504 classes={`icon-inline ${post_view.saved && "text-warning"}`}
511 {/* This is an expanding spacer for mobile */}
512 <div className="flex-grow-1"></div>
517 className={`btn-animate btn py-0 px-1 ${
518 this.state.my_vote == 1 ? "text-info" : "text-muted"
520 data-tippy-content={this.pointsTippy}
521 onClick={linkEvent(this, this.handlePostLike)}
522 aria-label={i18n.t("upvote")}
524 <Icon icon="arrow-up1" classes="icon-inline small mr-2" />
527 {this.props.enableDownvotes && (
529 className={`ml-2 btn-animate btn py-0 pl-1 ${
530 this.state.my_vote == -1 ? "text-danger" : "text-muted"
532 onClick={linkEvent(this, this.handlePostDisLike)}
533 data-tippy-content={this.pointsTippy}
534 aria-label={i18n.t("downvote")}
536 <Icon icon="arrow-down1" classes="icon-inline small mr-2" />
537 {this.state.downvotes !== 0 && (
538 <span>{this.state.downvotes}</span>
544 class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
545 onClick={linkEvent(this, this.handleSavePostClick)}
546 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
548 post_view.saved ? i18n.t("unsave") : i18n.t("save")
553 classes={`icon-inline ${post_view.saved && "text-warning"}`}
557 {!this.state.showMoreMobile && this.props.showBody && (
559 class="btn btn-link btn-animate text-muted py-0"
560 onClick={linkEvent(this, this.handleShowMoreMobile)}
561 aria-label={i18n.t("more")}
562 data-tippy-content={i18n.t("more")}
564 <Icon icon="more-vertical" classes="icon-inline" />
567 {this.state.showMoreMobile && this.postActions(mobile)}
575 let dupes = this.props.duplicates;
578 dupes.length > 0 && (
579 <ul class="list-inline mb-1 small text-muted">
581 <li className="list-inline-item mr-2">
582 {i18n.t("cross_posted_to")}
585 <li className="list-inline-item mr-2">
586 <Link to={`/post/${pv.post.id}`}>
589 : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
599 postActions(mobile = false) {
600 let post_view = this.props.post_view;
602 UserService.Instance.localUserView && (
604 {this.props.showBody && (
608 class="btn btn-link btn-animate text-muted py-0 pl-0"
609 onClick={linkEvent(this, this.handleSavePostClick)}
611 post_view.saved ? i18n.t("unsave") : i18n.t("save")
614 post_view.saved ? i18n.t("unsave") : i18n.t("save")
619 classes={`icon-inline ${post_view.saved && "text-warning"}`}
624 className="btn btn-link btn-animate text-muted py-0"
625 to={`/create_post${this.crossPostParams}`}
626 title={i18n.t("cross_post")}
628 <Icon icon="copy" classes="icon-inline" />
632 {this.myPost && this.props.showBody && (
635 class="btn btn-link btn-animate text-muted py-0"
636 onClick={linkEvent(this, this.handleEditClick)}
637 data-tippy-content={i18n.t("edit")}
638 aria-label={i18n.t("edit")}
640 <Icon icon="edit" classes="icon-inline" />
643 class="btn btn-link btn-animate text-muted py-0"
644 onClick={linkEvent(this, this.handleDeleteClick)}
646 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
649 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
654 classes={`icon-inline ${
655 post_view.post.deleted && "text-danger"
662 {!this.state.showAdvanced && this.props.showBody ? (
664 class="btn btn-link btn-animate text-muted py-0"
665 onClick={linkEvent(this, this.handleShowAdvanced)}
666 data-tippy-content={i18n.t("more")}
667 aria-label={i18n.t("more")}
669 <Icon icon="more-vertical" classes="icon-inline" />
673 {this.props.showBody && post_view.post.body && (
675 class="btn btn-link btn-animate text-muted py-0"
676 onClick={linkEvent(this, this.handleViewSource)}
677 data-tippy-content={i18n.t("view_source")}
678 aria-label={i18n.t("view_source")}
682 classes={`icon-inline ${
683 this.state.viewSource && "text-success"
688 {this.canModOnSelf && (
691 class="btn btn-link btn-animate text-muted py-0"
692 onClick={linkEvent(this, this.handleModLock)}
694 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
697 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
702 classes={`icon-inline ${
703 post_view.post.locked && "text-danger"
708 class="btn btn-link btn-animate text-muted py-0"
709 onClick={linkEvent(this, this.handleModSticky)}
711 post_view.post.stickied
716 post_view.post.stickied
723 classes={`icon-inline ${
724 post_view.post.stickied && "text-success"
730 {/* Mods can ban from community, and appoint as mods to community */}
731 {(this.canMod || this.canAdmin) &&
732 (!post_view.post.removed ? (
734 class="btn btn-link btn-animate text-muted py-0"
735 onClick={linkEvent(this, this.handleModRemoveShow)}
736 aria-label={i18n.t("remove")}
742 class="btn btn-link btn-animate text-muted py-0"
743 onClick={linkEvent(this, this.handleModRemoveSubmit)}
744 aria-label={i18n.t("restore")}
752 (!post_view.creator_banned_from_community ? (
754 class="btn btn-link btn-animate text-muted py-0"
757 this.handleModBanFromCommunityShow
759 aria-label={i18n.t("ban")}
765 class="btn btn-link btn-animate text-muted py-0"
768 this.handleModBanFromCommunitySubmit
770 aria-label={i18n.t("unban")}
775 {!post_view.creator_banned_from_community &&
776 post_view.creator.local && (
778 class="btn btn-link btn-animate text-muted py-0"
779 onClick={linkEvent(this, this.handleAddModToCommunity)}
782 ? i18n.t("remove_as_mod")
783 : i18n.t("appoint_as_mod")
787 ? i18n.t("remove_as_mod")
788 : i18n.t("appoint_as_mod")}
793 {/* Community creators and admins can transfer community to another mod */}
794 {(this.amCommunityCreator || this.canAdmin) &&
796 post_view.creator.local &&
797 (!this.state.showConfirmTransferCommunity ? (
799 class="btn btn-link btn-animate text-muted py-0"
802 this.handleShowConfirmTransferCommunity
804 aria-label={i18n.t("transfer_community")}
806 {i18n.t("transfer_community")}
811 class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
812 aria-label={i18n.t("are_you_sure")}
814 {i18n.t("are_you_sure")}
817 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
818 aria-label={i18n.t("yes")}
819 onClick={linkEvent(this, this.handleTransferCommunity)}
824 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
827 this.handleCancelShowConfirmTransferCommunity
829 aria-label={i18n.t("no")}
835 {/* Admins can ban from all, and appoint other admins */}
839 (!post_view.creator.banned ? (
841 class="btn btn-link btn-animate text-muted py-0"
842 onClick={linkEvent(this, this.handleModBanShow)}
843 aria-label={i18n.t("ban_from_site")}
845 {i18n.t("ban_from_site")}
849 class="btn btn-link btn-animate text-muted py-0"
850 onClick={linkEvent(this, this.handleModBanSubmit)}
851 aria-label={i18n.t("unban_from_site")}
853 {i18n.t("unban_from_site")}
856 {!post_view.creator.banned && post_view.creator.local && (
858 class="btn btn-link btn-animate text-muted py-0"
859 onClick={linkEvent(this, this.handleAddAdmin)}
862 ? i18n.t("remove_as_admin")
863 : i18n.t("appoint_as_admin")
867 ? i18n.t("remove_as_admin")
868 : i18n.t("appoint_as_admin")}
873 {/* Site Creator can transfer to another admin */}
874 {this.amSiteCreator &&
876 (!this.state.showConfirmTransferSite ? (
878 class="btn btn-link btn-animate text-muted py-0"
881 this.handleShowConfirmTransferSite
883 aria-label={i18n.t("transfer_site")}
885 {i18n.t("transfer_site")}
890 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
891 aria-label={i18n.t("are_you_sure")}
893 {i18n.t("are_you_sure")}
896 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
897 onClick={linkEvent(this, this.handleTransferSite)}
898 aria-label={i18n.t("yes")}
903 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
906 this.handleCancelShowConfirmTransferSite
908 aria-label={i18n.t("no")}
921 removeAndBanDialogs() {
922 let post = this.props.post_view;
925 {this.state.showRemoveDialog && (
928 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
930 <label class="sr-only" htmlFor="post-listing-remove-reason">
935 id="post-listing-remove-reason"
936 class="form-control mr-2"
937 placeholder={i18n.t("reason")}
938 value={this.state.removeReason}
939 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
943 class="btn btn-secondary"
944 aria-label={i18n.t("remove_post")}
946 {i18n.t("remove_post")}
950 {this.state.showBanDialog && (
951 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
952 <div class="form-group row">
953 <label class="col-form-label" htmlFor="post-listing-ban-reason">
958 id="post-listing-ban-reason"
959 class="form-control mr-2"
960 placeholder={i18n.t("reason")}
961 value={this.state.banReason}
962 onInput={linkEvent(this, this.handleModBanReasonChange)}
964 <div class="form-group">
965 <div class="form-check">
967 class="form-check-input"
968 id="mod-ban-remove-data"
970 checked={this.state.removeData}
971 onChange={linkEvent(this, this.handleModRemoveDataChange)}
973 <label class="form-check-label" htmlFor="mod-ban-remove-data">
974 {i18n.t("remove_posts_comments")}
979 {/* TODO hold off on expires until later */}
980 {/* <div class="form-group row"> */}
981 {/* <label class="col-form-label">Expires</label> */}
982 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
984 <div class="form-group row">
987 class="btn btn-secondary"
988 aria-label={i18n.t("ban")}
990 {i18n.t("ban")} {post.creator.name}
1000 let post = this.props.post_view.post;
1001 return post.thumbnail_url || isImage(post.url) ? (
1003 <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1004 {this.postTitleLine()}
1007 {/* Post body prev or thumbnail */}
1008 {!this.state.imageExpanded && this.thumbnail()}
1012 this.postTitleLine()
1016 showMobilePreview() {
1017 let post = this.props.post_view.post;
1020 !this.props.showBody && (
1022 className="md-div mb-1"
1023 dangerouslySetInnerHTML={{
1024 __html: md.render(previewLines(post.body)),
1034 {/* The mobile view*/}
1035 <div class="d-block d-sm-none">
1037 <div class="col-12">
1038 {this.createdLine()}
1040 {/* If it has a thumbnail, do a right aligned thumbnail */}
1041 {this.mobileThumbnail()}
1043 {/* Show a preview of the post body */}
1044 {this.showMobilePreview()}
1046 {this.commentsLine(true)}
1047 {this.duplicatesLine()}
1048 {this.removeAndBanDialogs()}
1053 {/* The larger view*/}
1054 <div class="d-none d-sm-block">
1057 {!this.state.imageExpanded && (
1058 <div class="col-sm-2 pr-0">
1059 <div class="">{this.thumbnail()}</div>
1064 this.state.imageExpanded ? "col-12" : "col-12 col-sm-9"
1068 <div className="col-12">
1069 {this.postTitleLine()}
1070 {this.createdLine()}
1071 {this.commentsLine()}
1072 {this.duplicatesLine()}
1073 {this.postActions()}
1074 {this.removeAndBanDialogs()}
1084 private get myPost(): boolean {
1086 UserService.Instance.localUserView &&
1087 this.props.post_view.creator.id ==
1088 UserService.Instance.localUserView.person.id
1092 get isMod(): boolean {
1094 this.props.moderators &&
1096 this.props.moderators.map(m => m.moderator.id),
1097 this.props.post_view.creator.id
1102 get isAdmin(): boolean {
1104 this.props.admins &&
1106 this.props.admins.map(a => a.person.id),
1107 this.props.post_view.creator.id
1112 get canMod(): boolean {
1113 if (this.props.admins && this.props.moderators) {
1114 let adminsThenMods = this.props.admins
1115 .map(a => a.person.id)
1116 .concat(this.props.moderators.map(m => m.moderator.id));
1119 UserService.Instance.localUserView,
1121 this.props.post_view.creator.id
1128 get canModOnSelf(): boolean {
1129 if (this.props.admins && this.props.moderators) {
1130 let adminsThenMods = this.props.admins
1131 .map(a => a.person.id)
1132 .concat(this.props.moderators.map(m => m.moderator.id));
1135 UserService.Instance.localUserView,
1137 this.props.post_view.creator.id,
1145 get canAdmin(): boolean {
1147 this.props.admins &&
1149 UserService.Instance.localUserView,
1150 this.props.admins.map(a => a.person.id),
1151 this.props.post_view.creator.id
1156 get amCommunityCreator(): boolean {
1158 this.props.moderators &&
1159 UserService.Instance.localUserView &&
1160 this.props.post_view.creator.id !=
1161 UserService.Instance.localUserView.person.id &&
1162 UserService.Instance.localUserView.person.id ==
1163 this.props.moderators[0].moderator.id
1167 get amSiteCreator(): boolean {
1169 this.props.admins &&
1170 UserService.Instance.localUserView &&
1171 this.props.post_view.creator.id !=
1172 UserService.Instance.localUserView.person.id &&
1173 UserService.Instance.localUserView.person.id ==
1174 this.props.admins[0].person.id
1178 handlePostLike(i: PostListing, event: any) {
1179 event.preventDefault();
1180 if (!UserService.Instance.localUserView) {
1181 this.context.router.history.push(`/login`);
1184 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1186 if (i.state.my_vote == 1) {
1189 } else if (i.state.my_vote == -1) {
1190 i.state.downvotes--;
1198 i.state.my_vote = new_vote;
1200 let form: CreatePostLike = {
1201 post_id: i.props.post_view.post.id,
1202 score: i.state.my_vote,
1206 WebSocketService.Instance.send(wsClient.likePost(form));
1207 i.setState(i.state);
1211 handlePostDisLike(i: PostListing, event: any) {
1212 event.preventDefault();
1213 if (!UserService.Instance.localUserView) {
1214 this.context.router.history.push(`/login`);
1217 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1219 if (i.state.my_vote == 1) {
1222 i.state.downvotes++;
1223 } else if (i.state.my_vote == -1) {
1224 i.state.downvotes--;
1227 i.state.downvotes++;
1231 i.state.my_vote = new_vote;
1233 let form: CreatePostLike = {
1234 post_id: i.props.post_view.post.id,
1235 score: i.state.my_vote,
1239 WebSocketService.Instance.send(wsClient.likePost(form));
1240 i.setState(i.state);
1244 handleEditClick(i: PostListing) {
1245 i.state.showEdit = true;
1246 i.setState(i.state);
1249 handleEditCancel() {
1250 this.state.showEdit = false;
1251 this.setState(this.state);
1254 // The actual editing is done in the recieve for post
1256 this.state.showEdit = false;
1257 this.setState(this.state);
1260 handleDeleteClick(i: PostListing) {
1261 let deleteForm: DeletePost = {
1262 post_id: i.props.post_view.post.id,
1263 deleted: !i.props.post_view.post.deleted,
1266 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1269 handleSavePostClick(i: PostListing) {
1271 i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1272 let form: SavePost = {
1273 post_id: i.props.post_view.post.id,
1278 WebSocketService.Instance.send(wsClient.savePost(form));
1281 get crossPostParams(): string {
1282 let post = this.props.post_view.post;
1283 let params = `?title=${encodeURIComponent(post.name)}`;
1286 params += `&url=${encodeURIComponent(post.url)}`;
1289 params += `&body=${encodeURIComponent(post.body)}`;
1294 handleModRemoveShow(i: PostListing) {
1295 i.state.showRemoveDialog = true;
1296 i.setState(i.state);
1299 handleModRemoveReasonChange(i: PostListing, event: any) {
1300 i.state.removeReason = event.target.value;
1301 i.setState(i.state);
1304 handleModRemoveDataChange(i: PostListing, event: any) {
1305 i.state.removeData = event.target.checked;
1306 i.setState(i.state);
1309 handleModRemoveSubmit(i: PostListing, event: any) {
1310 event.preventDefault();
1311 let form: RemovePost = {
1312 post_id: i.props.post_view.post.id,
1313 removed: !i.props.post_view.post.removed,
1314 reason: i.state.removeReason,
1317 WebSocketService.Instance.send(wsClient.removePost(form));
1319 i.state.showRemoveDialog = false;
1320 i.setState(i.state);
1323 handleModLock(i: PostListing) {
1324 let form: LockPost = {
1325 post_id: i.props.post_view.post.id,
1326 locked: !i.props.post_view.post.locked,
1329 WebSocketService.Instance.send(wsClient.lockPost(form));
1332 handleModSticky(i: PostListing) {
1333 let form: StickyPost = {
1334 post_id: i.props.post_view.post.id,
1335 stickied: !i.props.post_view.post.stickied,
1338 WebSocketService.Instance.send(wsClient.stickyPost(form));
1341 handleModBanFromCommunityShow(i: PostListing) {
1342 i.state.showBanDialog = true;
1343 i.state.banType = BanType.Community;
1344 i.setState(i.state);
1347 handleModBanShow(i: PostListing) {
1348 i.state.showBanDialog = true;
1349 i.state.banType = BanType.Site;
1350 i.setState(i.state);
1353 handleModBanReasonChange(i: PostListing, event: any) {
1354 i.state.banReason = event.target.value;
1355 i.setState(i.state);
1358 handleModBanExpiresChange(i: PostListing, event: any) {
1359 i.state.banExpires = event.target.value;
1360 i.setState(i.state);
1363 handleModBanFromCommunitySubmit(i: PostListing) {
1364 i.state.banType = BanType.Community;
1365 i.setState(i.state);
1366 i.handleModBanBothSubmit(i);
1369 handleModBanSubmit(i: PostListing) {
1370 i.state.banType = BanType.Site;
1371 i.setState(i.state);
1372 i.handleModBanBothSubmit(i);
1375 handleModBanBothSubmit(i: PostListing, event?: any) {
1376 event.preventDefault();
1378 if (i.state.banType == BanType.Community) {
1379 // If its an unban, restore all their data
1380 let ban = !i.props.post_view.creator_banned_from_community;
1382 i.state.removeData = false;
1384 let form: BanFromCommunity = {
1385 person_id: i.props.post_view.creator.id,
1386 community_id: i.props.post_view.community.id,
1388 remove_data: i.state.removeData,
1389 reason: i.state.banReason,
1390 expires: getUnixTime(i.state.banExpires),
1393 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1395 // If its an unban, restore all their data
1396 let ban = !i.props.post_view.creator.banned;
1398 i.state.removeData = false;
1400 let form: BanPerson = {
1401 person_id: i.props.post_view.creator.id,
1403 remove_data: i.state.removeData,
1404 reason: i.state.banReason,
1405 expires: getUnixTime(i.state.banExpires),
1408 WebSocketService.Instance.send(wsClient.banPerson(form));
1411 i.state.showBanDialog = false;
1412 i.setState(i.state);
1415 handleAddModToCommunity(i: PostListing) {
1416 let form: AddModToCommunity = {
1417 person_id: i.props.post_view.creator.id,
1418 community_id: i.props.post_view.community.id,
1422 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1423 i.setState(i.state);
1426 handleAddAdmin(i: PostListing) {
1427 let form: AddAdmin = {
1428 person_id: i.props.post_view.creator.id,
1432 WebSocketService.Instance.send(wsClient.addAdmin(form));
1433 i.setState(i.state);
1436 handleShowConfirmTransferCommunity(i: PostListing) {
1437 i.state.showConfirmTransferCommunity = true;
1438 i.setState(i.state);
1441 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1442 i.state.showConfirmTransferCommunity = false;
1443 i.setState(i.state);
1446 handleTransferCommunity(i: PostListing) {
1447 let form: TransferCommunity = {
1448 community_id: i.props.post_view.community.id,
1449 person_id: i.props.post_view.creator.id,
1452 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1453 i.state.showConfirmTransferCommunity = false;
1454 i.setState(i.state);
1457 handleShowConfirmTransferSite(i: PostListing) {
1458 i.state.showConfirmTransferSite = true;
1459 i.setState(i.state);
1462 handleCancelShowConfirmTransferSite(i: PostListing) {
1463 i.state.showConfirmTransferSite = false;
1464 i.setState(i.state);
1467 handleTransferSite(i: PostListing) {
1468 let form: TransferSite = {
1469 person_id: i.props.post_view.creator.id,
1472 WebSocketService.Instance.send(wsClient.transferSite(form));
1473 i.state.showConfirmTransferSite = false;
1474 i.setState(i.state);
1477 handleImageExpandClick(i: PostListing) {
1478 i.state.imageExpanded = !i.state.imageExpanded;
1479 i.setState(i.state);
1482 handleViewSource(i: PostListing) {
1483 i.state.viewSource = !i.state.viewSource;
1484 i.setState(i.state);
1487 handleShowAdvanced(i: PostListing) {
1488 i.state.showAdvanced = !i.state.showAdvanced;
1489 i.setState(i.state);
1493 handleShowMoreMobile(i: PostListing) {
1494 i.state.showMoreMobile = !i.state.showMoreMobile;
1495 i.state.showAdvanced = !i.state.showAdvanced;
1496 i.setState(i.state);
1500 get pointsTippy(): string {
1501 let points = i18n.t("number_of_points", {
1502 count: this.state.score,
1505 let upvotes = i18n.t("number_of_upvotes", {
1506 count: this.state.upvotes,
1509 let downvotes = i18n.t("number_of_downvotes", {
1510 count: this.state.downvotes,
1513 return `${points} • ${upvotes} • ${downvotes}`;