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 href={this.getImageSrc()}
208 class="float-right text-body d-inline-block position-relative mb-2"
209 data-tippy-content={i18n.t("expand_here")}
210 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 href={this.getImageSrc()}
424 class="btn btn-link d-inline-block"
425 onClick={linkEvent(this, this.handleImageExpandClick)}
427 <PictrsImage src={this.getImageSrc()} />
433 <small className="ml-2 text-muted font-italic">
439 className="unselectable pointer ml-2 text-muted font-italic"
440 data-tippy-content={i18n.t("deleted")}
442 <Icon icon="trash" classes="icon-inline text-danger" />
447 className="unselectable pointer ml-2 text-muted font-italic"
448 data-tippy-content={i18n.t("locked")}
450 <Icon icon="lock" classes="icon-inline text-danger" />
455 className="unselectable pointer ml-2 text-muted font-italic"
456 data-tippy-content={i18n.t("stickied")}
458 <Icon icon="pin" classes="icon-inline text-primary" />
462 <small className="ml-2 text-muted font-italic">
471 commentsLine(mobile = false) {
472 let post_view = this.props.post_view;
474 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
475 <button class="btn btn-link text-muted p-0">
477 className="text-muted small"
478 title={i18n.t("number_of_comments", {
479 count: post_view.counts.comments,
480 formattedCount: post_view.counts.comments,
482 to={`/post/${post_view.post.id}?scrollToComments=true`}
484 <Icon icon="message-square" classes="icon-inline mr-1" />
485 {i18n.t("number_of_comments", {
486 count: post_view.counts.comments,
487 formattedCount: numToSI(post_view.counts.comments),
493 {this.state.downvotes !== 0 && showScores() && (
495 class="btn text-muted py-0 pr-0"
496 data-tippy-content={this.pointsTippy}
497 aria-label={i18n.t("downvote")}
500 <Icon icon="arrow-down1" classes="icon-inline mr-1" />
501 <span>{numToSI(this.state.downvotes)}</span>
507 class="btn btn-link btn-animate text-muted py-0"
508 onClick={linkEvent(this, this.handleSavePostClick)}
510 post_view.saved ? i18n.t("unsave") : i18n.t("save")
512 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
517 classes={`icon-inline ${post_view.saved && "text-warning"}`}
524 {/* This is an expanding spacer for mobile */}
525 <div className="flex-grow-1"></div>
531 className={`btn-animate btn py-0 px-1 ${
532 this.state.my_vote == 1 ? "text-info" : "text-muted"
534 data-tippy-content={this.pointsTippy}
535 onClick={linkEvent(this, this.handlePostLike)}
536 aria-label={i18n.t("upvote")}
538 <Icon icon="arrow-up1" classes="icon-inline small mr-2" />
539 {numToSI(this.state.upvotes)}
543 className={`btn-animate btn py-0 px-1 ${
544 this.state.my_vote == 1 ? "text-info" : "text-muted"
546 onClick={linkEvent(this, this.handlePostLike)}
547 aria-label={i18n.t("upvote")}
549 <Icon icon="arrow-up1" classes="icon-inline small" />
552 {this.props.enableDownvotes &&
555 className={`ml-2 btn-animate btn py-0 pl-1 ${
556 this.state.my_vote == -1 ? "text-danger" : "text-muted"
558 onClick={linkEvent(this, this.handlePostDisLike)}
559 data-tippy-content={this.pointsTippy}
560 aria-label={i18n.t("downvote")}
562 <Icon icon="arrow-down1" classes="icon-inline small mr-2" />
563 {this.state.downvotes !== 0 && (
564 <span>{numToSI(this.state.downvotes)}</span>
569 className={`ml-2 btn-animate btn py-0 pl-1 ${
570 this.state.my_vote == -1 ? "text-danger" : "text-muted"
572 onClick={linkEvent(this, this.handlePostDisLike)}
573 aria-label={i18n.t("downvote")}
575 <Icon icon="arrow-down1" classes="icon-inline small" />
580 class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
581 onClick={linkEvent(this, this.handleSavePostClick)}
582 aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
584 post_view.saved ? i18n.t("unsave") : i18n.t("save")
589 classes={`icon-inline ${post_view.saved && "text-warning"}`}
593 {!this.state.showMoreMobile && this.showBody && (
595 class="btn btn-link btn-animate text-muted py-0"
596 onClick={linkEvent(this, this.handleShowMoreMobile)}
597 aria-label={i18n.t("more")}
598 data-tippy-content={i18n.t("more")}
600 <Icon icon="more-vertical" classes="icon-inline" />
603 {this.state.showMoreMobile && this.postActions(mobile)}
611 let dupes = this.props.duplicates;
614 dupes.length > 0 && (
615 <ul class="list-inline mb-1 small text-muted">
617 <li className="list-inline-item mr-2">
618 {i18n.t("cross_posted_to")}
621 <li className="list-inline-item mr-2">
622 <Link to={`/post/${pv.post.id}`}>
625 : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
635 postActions(mobile = false) {
636 let post_view = this.props.post_view;
638 UserService.Instance.myUserInfo && (
644 class="btn btn-link btn-animate text-muted py-0 pl-0"
645 onClick={linkEvent(this, this.handleSavePostClick)}
647 post_view.saved ? i18n.t("unsave") : i18n.t("save")
650 post_view.saved ? i18n.t("unsave") : i18n.t("save")
655 classes={`icon-inline ${post_view.saved && "text-warning"}`}
660 className="btn btn-link btn-animate text-muted py-0"
661 to={`/create_post${this.crossPostParams}`}
662 title={i18n.t("cross_post")}
664 <Icon icon="copy" classes="icon-inline" />
668 class="btn btn-link btn-animate text-muted py-0"
669 onClick={linkEvent(this, this.handleBlockUserClick)}
670 data-tippy-content={i18n.t("block_user")}
671 aria-label={i18n.t("block_user")}
673 <Icon icon="slash" classes="icon-inline" />
678 {this.myPost && this.showBody && (
681 class="btn btn-link btn-animate text-muted py-0"
682 onClick={linkEvent(this, this.handleEditClick)}
683 data-tippy-content={i18n.t("edit")}
684 aria-label={i18n.t("edit")}
686 <Icon icon="edit" classes="icon-inline" />
689 class="btn btn-link btn-animate text-muted py-0"
690 onClick={linkEvent(this, this.handleDeleteClick)}
692 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
695 !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
700 classes={`icon-inline ${
701 post_view.post.deleted && "text-danger"
708 {!this.state.showAdvanced && this.showBody ? (
710 class="btn btn-link btn-animate text-muted py-0"
711 onClick={linkEvent(this, this.handleShowAdvanced)}
712 data-tippy-content={i18n.t("more")}
713 aria-label={i18n.t("more")}
715 <Icon icon="more-vertical" classes="icon-inline" />
719 {this.showBody && post_view.post.body && (
721 class="btn btn-link btn-animate text-muted py-0"
722 onClick={linkEvent(this, this.handleViewSource)}
723 data-tippy-content={i18n.t("view_source")}
724 aria-label={i18n.t("view_source")}
728 classes={`icon-inline ${
729 this.state.viewSource && "text-success"
734 {this.canModOnSelf && (
737 class="btn btn-link btn-animate text-muted py-0"
738 onClick={linkEvent(this, this.handleModLock)}
740 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
743 post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
748 classes={`icon-inline ${
749 post_view.post.locked && "text-danger"
754 class="btn btn-link btn-animate text-muted py-0"
755 onClick={linkEvent(this, this.handleModSticky)}
757 post_view.post.stickied
762 post_view.post.stickied
769 classes={`icon-inline ${
770 post_view.post.stickied && "text-success"
776 {/* Mods can ban from community, and appoint as mods to community */}
777 {(this.canMod || this.canAdmin) &&
778 (!post_view.post.removed ? (
780 class="btn btn-link btn-animate text-muted py-0"
781 onClick={linkEvent(this, this.handleModRemoveShow)}
782 aria-label={i18n.t("remove")}
788 class="btn btn-link btn-animate text-muted py-0"
789 onClick={linkEvent(this, this.handleModRemoveSubmit)}
790 aria-label={i18n.t("restore")}
798 (!post_view.creator_banned_from_community ? (
800 class="btn btn-link btn-animate text-muted py-0"
803 this.handleModBanFromCommunityShow
805 aria-label={i18n.t("ban")}
811 class="btn btn-link btn-animate text-muted py-0"
814 this.handleModBanFromCommunitySubmit
816 aria-label={i18n.t("unban")}
821 {!post_view.creator_banned_from_community && (
823 class="btn btn-link btn-animate text-muted py-0"
824 onClick={linkEvent(this, this.handleAddModToCommunity)}
827 ? i18n.t("remove_as_mod")
828 : i18n.t("appoint_as_mod")
832 ? i18n.t("remove_as_mod")
833 : i18n.t("appoint_as_mod")}
838 {/* Community creators and admins can transfer community to another mod */}
839 {(this.amCommunityCreator || this.canAdmin) &&
841 (!this.state.showConfirmTransferCommunity ? (
843 class="btn btn-link btn-animate text-muted py-0"
846 this.handleShowConfirmTransferCommunity
848 aria-label={i18n.t("transfer_community")}
850 {i18n.t("transfer_community")}
855 class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
856 aria-label={i18n.t("are_you_sure")}
858 {i18n.t("are_you_sure")}
861 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
862 aria-label={i18n.t("yes")}
863 onClick={linkEvent(this, this.handleTransferCommunity)}
868 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
871 this.handleCancelShowConfirmTransferCommunity
873 aria-label={i18n.t("no")}
879 {/* Admins can ban from all, and appoint other admins */}
883 (!post_view.creator.banned ? (
885 class="btn btn-link btn-animate text-muted py-0"
886 onClick={linkEvent(this, this.handleModBanShow)}
887 aria-label={i18n.t("ban_from_site")}
889 {i18n.t("ban_from_site")}
893 class="btn btn-link btn-animate text-muted py-0"
894 onClick={linkEvent(this, this.handleModBanSubmit)}
895 aria-label={i18n.t("unban_from_site")}
897 {i18n.t("unban_from_site")}
900 {!post_view.creator.banned && post_view.creator.local && (
902 class="btn btn-link btn-animate text-muted py-0"
903 onClick={linkEvent(this, this.handleAddAdmin)}
906 ? i18n.t("remove_as_admin")
907 : i18n.t("appoint_as_admin")
911 ? i18n.t("remove_as_admin")
912 : i18n.t("appoint_as_admin")}
917 {/* Site Creator can transfer to another admin */}
918 {this.amSiteCreator &&
920 (!this.state.showConfirmTransferSite ? (
922 class="btn btn-link btn-animate text-muted py-0"
925 this.handleShowConfirmTransferSite
927 aria-label={i18n.t("transfer_site")}
929 {i18n.t("transfer_site")}
934 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
935 aria-label={i18n.t("are_you_sure")}
937 {i18n.t("are_you_sure")}
940 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
941 onClick={linkEvent(this, this.handleTransferSite)}
942 aria-label={i18n.t("yes")}
947 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
950 this.handleCancelShowConfirmTransferSite
952 aria-label={i18n.t("no")}
965 removeAndBanDialogs() {
966 let post = this.props.post_view;
969 {this.state.showRemoveDialog && (
972 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
974 <label class="sr-only" htmlFor="post-listing-remove-reason">
979 id="post-listing-remove-reason"
980 class="form-control mr-2"
981 placeholder={i18n.t("reason")}
982 value={this.state.removeReason}
983 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
987 class="btn btn-secondary"
988 aria-label={i18n.t("remove_post")}
990 {i18n.t("remove_post")}
994 {this.state.showBanDialog && (
995 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
996 <div class="form-group row">
997 <label class="col-form-label" htmlFor="post-listing-ban-reason">
1002 id="post-listing-ban-reason"
1003 class="form-control mr-2"
1004 placeholder={i18n.t("reason")}
1005 value={this.state.banReason}
1006 onInput={linkEvent(this, this.handleModBanReasonChange)}
1008 <div class="form-group">
1009 <div class="form-check">
1011 class="form-check-input"
1012 id="mod-ban-remove-data"
1014 checked={this.state.removeData}
1015 onChange={linkEvent(this, this.handleModRemoveDataChange)}
1018 class="form-check-label"
1019 htmlFor="mod-ban-remove-data"
1020 title={i18n.t("remove_content_more")}
1022 {i18n.t("remove_content")}
1027 {/* TODO hold off on expires until later */}
1028 {/* <div class="form-group row"> */}
1029 {/* <label class="col-form-label">Expires</label> */}
1030 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
1032 <div class="form-group row">
1035 class="btn btn-secondary"
1036 aria-label={i18n.t("ban")}
1038 {i18n.t("ban")} {post.creator.name}
1048 let post = this.props.post_view.post;
1049 return post.thumbnail_url || isImage(post.url) ? (
1051 <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
1052 {this.postTitleLine()}
1055 {/* Post body prev or thumbnail */}
1056 {!this.state.imageExpanded && this.thumbnail()}
1060 this.postTitleLine()
1064 showMobilePreview() {
1065 let post = this.props.post_view.post;
1070 className="md-div mb-1"
1071 dangerouslySetInnerHTML={{
1072 __html: md.render(previewLines(post.body)),
1082 {/* The mobile view*/}
1083 <div class="d-block d-sm-none">
1085 <div class="col-12">
1086 {this.createdLine()}
1088 {/* If it has a thumbnail, do a right aligned thumbnail */}
1089 {this.mobileThumbnail()}
1091 {/* Show a preview of the post body */}
1092 {this.showMobilePreview()}
1094 {this.commentsLine(true)}
1095 {this.duplicatesLine()}
1096 {this.removeAndBanDialogs()}
1101 {/* The larger view*/}
1102 <div class="d-none d-sm-block">
1105 {!this.state.imageExpanded && (
1106 <div class="col-sm-2 pr-0">
1107 <div class="">{this.thumbnail()}</div>
1112 this.state.imageExpanded ? "col-12" : "col-12 col-sm-9"
1116 <div className="col-12">
1117 {this.postTitleLine()}
1118 {this.createdLine()}
1119 {this.commentsLine()}
1120 {this.duplicatesLine()}
1121 {this.postActions()}
1122 {this.removeAndBanDialogs()}
1132 private get myPost(): boolean {
1134 UserService.Instance.myUserInfo &&
1135 this.props.post_view.creator.id ==
1136 UserService.Instance.myUserInfo.local_user_view.person.id
1140 get isMod(): boolean {
1142 this.props.moderators &&
1144 this.props.moderators.map(m => m.moderator.id),
1145 this.props.post_view.creator.id
1150 get isAdmin(): boolean {
1152 this.props.admins &&
1154 this.props.admins.map(a => a.person.id),
1155 this.props.post_view.creator.id
1160 get canMod(): boolean {
1161 if (this.props.admins && this.props.moderators) {
1162 let adminsThenMods = this.props.admins
1163 .map(a => a.person.id)
1164 .concat(this.props.moderators.map(m => m.moderator.id));
1167 UserService.Instance.myUserInfo,
1169 this.props.post_view.creator.id
1176 get canModOnSelf(): boolean {
1177 if (this.props.admins && this.props.moderators) {
1178 let adminsThenMods = this.props.admins
1179 .map(a => a.person.id)
1180 .concat(this.props.moderators.map(m => m.moderator.id));
1183 UserService.Instance.myUserInfo,
1185 this.props.post_view.creator.id,
1193 get canAdmin(): boolean {
1195 this.props.admins &&
1197 UserService.Instance.myUserInfo,
1198 this.props.admins.map(a => a.person.id),
1199 this.props.post_view.creator.id
1204 get amCommunityCreator(): boolean {
1206 this.props.moderators &&
1207 UserService.Instance.myUserInfo &&
1208 this.props.post_view.creator.id !=
1209 UserService.Instance.myUserInfo.local_user_view.person.id &&
1210 UserService.Instance.myUserInfo.local_user_view.person.id ==
1211 this.props.moderators[0].moderator.id
1215 get amSiteCreator(): boolean {
1217 this.props.admins &&
1218 UserService.Instance.myUserInfo &&
1219 this.props.post_view.creator.id !=
1220 UserService.Instance.myUserInfo.local_user_view.person.id &&
1221 UserService.Instance.myUserInfo.local_user_view.person.id ==
1222 this.props.admins[0].person.id
1226 handlePostLike(i: PostListing, event: any) {
1227 event.preventDefault();
1228 if (!UserService.Instance.myUserInfo) {
1229 this.context.router.history.push(`/login`);
1232 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1234 if (i.state.my_vote == 1) {
1237 } else if (i.state.my_vote == -1) {
1238 i.state.downvotes--;
1246 i.state.my_vote = new_vote;
1248 let form: CreatePostLike = {
1249 post_id: i.props.post_view.post.id,
1250 score: i.state.my_vote,
1254 WebSocketService.Instance.send(wsClient.likePost(form));
1255 i.setState(i.state);
1259 handlePostDisLike(i: PostListing, event: any) {
1260 event.preventDefault();
1261 if (!UserService.Instance.myUserInfo) {
1262 this.context.router.history.push(`/login`);
1265 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1267 if (i.state.my_vote == 1) {
1270 i.state.downvotes++;
1271 } else if (i.state.my_vote == -1) {
1272 i.state.downvotes--;
1275 i.state.downvotes++;
1279 i.state.my_vote = new_vote;
1281 let form: CreatePostLike = {
1282 post_id: i.props.post_view.post.id,
1283 score: i.state.my_vote,
1287 WebSocketService.Instance.send(wsClient.likePost(form));
1288 i.setState(i.state);
1292 handleEditClick(i: PostListing) {
1293 i.state.showEdit = true;
1294 i.setState(i.state);
1297 handleEditCancel() {
1298 this.state.showEdit = false;
1299 this.setState(this.state);
1302 // The actual editing is done in the recieve for post
1304 this.state.showEdit = false;
1305 this.setState(this.state);
1308 handleBlockUserClick(i: PostListing) {
1309 let blockUserForm: BlockPerson = {
1310 person_id: i.props.post_view.creator.id,
1314 WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
1317 handleDeleteClick(i: PostListing) {
1318 let deleteForm: DeletePost = {
1319 post_id: i.props.post_view.post.id,
1320 deleted: !i.props.post_view.post.deleted,
1323 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1326 handleSavePostClick(i: PostListing) {
1328 i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1329 let form: SavePost = {
1330 post_id: i.props.post_view.post.id,
1335 WebSocketService.Instance.send(wsClient.savePost(form));
1338 get crossPostParams(): string {
1339 let post = this.props.post_view.post;
1340 let params = `?title=${encodeURIComponent(post.name)}`;
1343 params += `&url=${encodeURIComponent(post.url)}`;
1346 params += `&body=${encodeURIComponent(this.crossPostBody())}`;
1351 crossPostBody(): string {
1352 let post = this.props.post_view.post;
1353 let body = `${i18n.t("cross_posted_from")} ${
1355 }\n\n${post.body.replace(/^/gm, "> ")}`;
1359 get showBody(): boolean {
1360 return this.props.showBody || this.state.showBody;
1363 handleModRemoveShow(i: PostListing) {
1364 i.state.showRemoveDialog = true;
1365 i.setState(i.state);
1368 handleModRemoveReasonChange(i: PostListing, event: any) {
1369 i.state.removeReason = event.target.value;
1370 i.setState(i.state);
1373 handleModRemoveDataChange(i: PostListing, event: any) {
1374 i.state.removeData = event.target.checked;
1375 i.setState(i.state);
1378 handleModRemoveSubmit(i: PostListing, event: any) {
1379 event.preventDefault();
1380 let form: RemovePost = {
1381 post_id: i.props.post_view.post.id,
1382 removed: !i.props.post_view.post.removed,
1383 reason: i.state.removeReason,
1386 WebSocketService.Instance.send(wsClient.removePost(form));
1388 i.state.showRemoveDialog = false;
1389 i.setState(i.state);
1392 handleModLock(i: PostListing) {
1393 let form: LockPost = {
1394 post_id: i.props.post_view.post.id,
1395 locked: !i.props.post_view.post.locked,
1398 WebSocketService.Instance.send(wsClient.lockPost(form));
1401 handleModSticky(i: PostListing) {
1402 let form: StickyPost = {
1403 post_id: i.props.post_view.post.id,
1404 stickied: !i.props.post_view.post.stickied,
1407 WebSocketService.Instance.send(wsClient.stickyPost(form));
1410 handleModBanFromCommunityShow(i: PostListing) {
1411 i.state.showBanDialog = true;
1412 i.state.banType = BanType.Community;
1413 i.setState(i.state);
1416 handleModBanShow(i: PostListing) {
1417 i.state.showBanDialog = true;
1418 i.state.banType = BanType.Site;
1419 i.setState(i.state);
1422 handleModBanReasonChange(i: PostListing, event: any) {
1423 i.state.banReason = event.target.value;
1424 i.setState(i.state);
1427 handleModBanExpiresChange(i: PostListing, event: any) {
1428 i.state.banExpires = event.target.value;
1429 i.setState(i.state);
1432 handleModBanFromCommunitySubmit(i: PostListing) {
1433 i.state.banType = BanType.Community;
1434 i.setState(i.state);
1435 i.handleModBanBothSubmit(i);
1438 handleModBanSubmit(i: PostListing) {
1439 i.state.banType = BanType.Site;
1440 i.setState(i.state);
1441 i.handleModBanBothSubmit(i);
1444 handleModBanBothSubmit(i: PostListing, event?: any) {
1445 if (event) event.preventDefault();
1447 if (i.state.banType == BanType.Community) {
1448 // If its an unban, restore all their data
1449 let ban = !i.props.post_view.creator_banned_from_community;
1451 i.state.removeData = false;
1453 let form: BanFromCommunity = {
1454 person_id: i.props.post_view.creator.id,
1455 community_id: i.props.post_view.community.id,
1457 remove_data: i.state.removeData,
1458 reason: i.state.banReason,
1459 expires: getUnixTime(i.state.banExpires),
1462 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1464 // If its an unban, restore all their data
1465 let ban = !i.props.post_view.creator.banned;
1467 i.state.removeData = false;
1469 let form: BanPerson = {
1470 person_id: i.props.post_view.creator.id,
1472 remove_data: i.state.removeData,
1473 reason: i.state.banReason,
1474 expires: getUnixTime(i.state.banExpires),
1477 WebSocketService.Instance.send(wsClient.banPerson(form));
1480 i.state.showBanDialog = false;
1481 i.setState(i.state);
1484 handleAddModToCommunity(i: PostListing) {
1485 let form: AddModToCommunity = {
1486 person_id: i.props.post_view.creator.id,
1487 community_id: i.props.post_view.community.id,
1491 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1492 i.setState(i.state);
1495 handleAddAdmin(i: PostListing) {
1496 let form: AddAdmin = {
1497 person_id: i.props.post_view.creator.id,
1501 WebSocketService.Instance.send(wsClient.addAdmin(form));
1502 i.setState(i.state);
1505 handleShowConfirmTransferCommunity(i: PostListing) {
1506 i.state.showConfirmTransferCommunity = true;
1507 i.setState(i.state);
1510 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1511 i.state.showConfirmTransferCommunity = false;
1512 i.setState(i.state);
1515 handleTransferCommunity(i: PostListing) {
1516 let form: TransferCommunity = {
1517 community_id: i.props.post_view.community.id,
1518 person_id: i.props.post_view.creator.id,
1521 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1522 i.state.showConfirmTransferCommunity = false;
1523 i.setState(i.state);
1526 handleShowConfirmTransferSite(i: PostListing) {
1527 i.state.showConfirmTransferSite = true;
1528 i.setState(i.state);
1531 handleCancelShowConfirmTransferSite(i: PostListing) {
1532 i.state.showConfirmTransferSite = false;
1533 i.setState(i.state);
1536 handleTransferSite(i: PostListing) {
1537 let form: TransferSite = {
1538 person_id: i.props.post_view.creator.id,
1541 WebSocketService.Instance.send(wsClient.transferSite(form));
1542 i.state.showConfirmTransferSite = false;
1543 i.setState(i.state);
1546 handleImageExpandClick(i: PostListing, event: any) {
1547 event.preventDefault();
1548 i.state.imageExpanded = !i.state.imageExpanded;
1549 i.setState(i.state);
1552 handleViewSource(i: PostListing) {
1553 i.state.viewSource = !i.state.viewSource;
1554 i.setState(i.state);
1557 handleShowAdvanced(i: PostListing) {
1558 i.state.showAdvanced = !i.state.showAdvanced;
1559 i.setState(i.state);
1563 handleShowMoreMobile(i: PostListing) {
1564 i.state.showMoreMobile = !i.state.showMoreMobile;
1565 i.state.showAdvanced = !i.state.showAdvanced;
1566 i.setState(i.state);
1570 handleShowBody(i: PostListing) {
1571 i.state.showBody = !i.state.showBody;
1572 i.setState(i.state);
1576 get pointsTippy(): string {
1577 let points = i18n.t("number_of_points", {
1578 count: this.state.score,
1579 formattedCount: this.state.score,
1582 let upvotes = i18n.t("number_of_upvotes", {
1583 count: this.state.upvotes,
1584 formattedCount: this.state.upvotes,
1587 let downvotes = i18n.t("number_of_downvotes", {
1588 count: this.state.downvotes,
1589 formattedCount: this.state.downvotes,
1592 return `${points} • ${upvotes} • ${downvotes}`;