1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { WebSocketService, UserService } from '../services';
16 AddModToCommunityForm,
19 TransferCommunityForm,
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 { UserListing } from './user-listing';
26 import { CommunityLink } from './community-link';
27 import { PictrsImage } from './pictrs-image';
40 import { i18n } from '../i18next';
41 import { externalHost } from '../env';
43 interface PostListingState {
45 showRemoveDialog: boolean;
47 showBanDialog: boolean;
52 showConfirmTransferSite: boolean;
53 showConfirmTransferCommunity: boolean;
54 imageExpanded: boolean;
56 showAdvanced: boolean;
57 showMoreMobile: boolean;
64 interface PostListingProps {
66 showCommunity?: boolean;
68 moderators?: CommunityUser[];
70 enableDownvotes: boolean;
74 export class PostListing extends Component<PostListingProps, PostListingState> {
75 private emptyState: PostListingState = {
77 showRemoveDialog: false,
83 banType: BanType.Community,
84 showConfirmTransferSite: false,
85 showConfirmTransferCommunity: false,
89 showMoreMobile: false,
90 my_vote: this.props.post.my_vote,
91 score: this.props.post.score,
92 upvotes: this.props.post.upvotes,
93 downvotes: this.props.post.downvotes,
96 constructor(props: any, context: any) {
97 super(props, context);
99 this.state = this.emptyState;
100 this.handlePostLike = this.handlePostLike.bind(this);
101 this.handlePostDisLike = this.handlePostDisLike.bind(this);
102 this.handleEditPost = this.handleEditPost.bind(this);
103 this.handleEditCancel = this.handleEditCancel.bind(this);
106 componentWillReceiveProps(nextProps: PostListingProps) {
107 this.state.my_vote = nextProps.post.my_vote;
108 this.state.upvotes = nextProps.post.upvotes;
109 this.state.downvotes = nextProps.post.downvotes;
110 this.state.score = nextProps.post.score;
111 if (this.props.post.id !== nextProps.post.id) {
112 this.state.imageExpanded = false;
114 this.setState(this.state);
120 {!this.state.showEdit ? (
128 post={this.props.post}
129 onEdit={this.handleEditPost}
130 onCancel={this.handleEditCancel}
131 enableNsfw={this.props.enableNsfw}
132 enableDownvotes={this.props.enableDownvotes}
144 {this.props.post.url &&
145 this.props.showBody &&
146 this.props.post.embed_title && (
147 <IFramelyCard post={this.props.post} />
149 {this.props.showBody &&
150 this.props.post.body &&
151 (this.state.viewSource ? (
152 <pre>{this.props.post.body}</pre>
156 dangerouslySetInnerHTML={mdToHtml(this.props.post.body)}
164 imgThumb(src: string) {
165 let post = this.props.post;
170 nsfw={post.nsfw || post.community_nsfw}
175 getImageSrc(): string {
176 let post = this.props.post;
177 if (isImage(post.url)) {
178 if (post.url.includes('pictrs')) {
180 } else if (post.thumbnail_url) {
181 return post.thumbnail_url;
185 } else if (post.thumbnail_url) {
186 return post.thumbnail_url;
193 let post = this.props.post;
195 if (isImage(post.url)) {
198 class="float-right text-body pointer d-inline-block position-relative mb-2"
199 data-tippy-content={i18n.t('expand_here')}
200 onClick={linkEvent(this, this.handleImageExpandClick)}
202 {this.imgThumb(this.getImageSrc())}
203 <svg class="icon mini-overlay">
204 <use xlinkHref="#icon-image"></use>
208 } else if (post.thumbnail_url) {
211 class="float-right text-body d-inline-block position-relative mb-2"
217 {this.imgThumb(this.getImageSrc())}
218 <svg class="icon mini-overlay">
219 <use xlinkHref="#icon-external-link"></use>
223 } else if (post.url) {
224 if (isVideo(post.url)) {
226 <div class="embed-responsive embed-responsive-16by9">
232 class="embed-responsive-item"
234 <source src={post.url} type="video/mp4" />
241 className="text-body"
247 <div class="thumbnail rounded bg-light d-flex justify-content-center">
248 <svg class="icon d-flex align-items-center">
249 <use xlinkHref="#icon-external-link"></use>
258 className="text-body"
259 to={`/post/${post.id}`}
260 title={i18n.t('comments')}
262 <div class="thumbnail rounded bg-light d-flex justify-content-center">
263 <svg class="icon d-flex align-items-center">
264 <use xlinkHref="#icon-message-square"></use>
273 let post = this.props.post;
275 <ul class="list-inline mb-1 text-muted small">
276 <li className="list-inline-item">
279 name: post.creator_name,
280 preferred_username: post.creator_preferred_username,
281 avatar: post.creator_avatar,
283 local: post.creator_local,
284 actor_id: post.creator_actor_id,
285 published: post.creator_published,
290 <span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
293 <span className="mx-1 badge badge-light">{i18n.t('admin')}</span>
295 {(post.banned_from_community || post.banned) && (
296 <span className="mx-1 badge badge-danger">{i18n.t('banned')}</span>
298 {this.props.showCommunity && (
300 <span class="mx-1"> {i18n.t('to')} </span>
303 name: post.community_name,
304 id: post.community_id,
305 local: post.community_local,
306 actor_id: post.community_actor_id,
307 icon: post.community_icon,
313 <li className="list-inline-item">•</li>
314 {post.url && !(hostname(post.url) == externalHost) && (
316 <li className="list-inline-item">
318 className="text-muted font-italic"
327 <li className="list-inline-item">•</li>
330 <li className="list-inline-item">
332 <MomentTime data={post} />
337 <li className="list-inline-item">•</li>
338 <li className="list-inline-item">
339 {/* Using a link with tippy doesn't work on touch devices unfortunately */}
341 className="text-muted"
342 data-tippy-content={md.render(previewLines(post.body))}
343 data-tippy-allowHtml={true}
344 to={`/post/${post.id}`}
346 <svg class="mr-1 icon icon-inline">
347 <use xlinkHref="#icon-book-open"></use>
359 <div className={`vote-bar col-1 pr-0 small text-center`}>
361 className={`btn-animate btn btn-link p-0 ${
362 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
364 onClick={linkEvent(this, this.handlePostLike)}
365 data-tippy-content={i18n.t('upvote')}
367 <svg class="icon upvote">
368 <use xlinkHref="#icon-arrow-up1"></use>
372 class={`unselectable pointer font-weight-bold text-muted px-1`}
373 data-tippy-content={this.pointsTippy}
377 {this.props.enableDownvotes && (
379 className={`btn-animate btn btn-link p-0 ${
380 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
382 onClick={linkEvent(this, this.handlePostDisLike)}
383 data-tippy-content={i18n.t('downvote')}
385 <svg class="icon downvote">
386 <use xlinkHref="#icon-arrow-down1"></use>
395 let post = this.props.post;
397 <div className="post-title overflow-hidden">
399 {this.props.showBody && post.url ? (
401 className={!post.stickied ? 'text-body' : 'text-primary'}
411 className={!post.stickied ? 'text-body' : 'text-primary'}
412 to={`/post/${post.id}`}
413 title={i18n.t('comments')}
418 {(isImage(post.url) || this.props.post.thumbnail_url) &&
419 (!this.state.imageExpanded ? (
421 class="text-monospace unselectable pointer ml-2 text-muted small"
422 data-tippy-content={i18n.t('expand_here')}
423 onClick={linkEvent(this, this.handleImageExpandClick)}
425 <svg class="icon icon-inline">
426 <use xlinkHref="#icon-plus-square"></use>
432 class="text-monospace unselectable pointer ml-2 text-muted small"
433 onClick={linkEvent(this, this.handleImageExpandClick)}
435 <svg class="icon icon-inline">
436 <use xlinkHref="#icon-minus-square"></use>
442 onClick={linkEvent(this, this.handleImageExpandClick)}
444 <PictrsImage src={this.getImageSrc()} />
450 <small className="ml-2 text-muted font-italic">
456 className="unselectable pointer ml-2 text-muted font-italic"
457 data-tippy-content={i18n.t('deleted')}
459 <svg class={`icon icon-inline text-danger`}>
460 <use xlinkHref="#icon-trash"></use>
466 className="unselectable pointer ml-2 text-muted font-italic"
467 data-tippy-content={i18n.t('locked')}
469 <svg class={`icon icon-inline text-danger`}>
470 <use xlinkHref="#icon-lock"></use>
476 className="unselectable pointer ml-2 text-muted font-italic"
477 data-tippy-content={i18n.t('stickied')}
479 <svg class={`icon icon-inline text-primary`}>
480 <use xlinkHref="#icon-pin"></use>
485 <small className="ml-2 text-muted font-italic">
494 commentsLine(mobile: boolean = false) {
495 let post = this.props.post;
497 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
498 <button class="btn btn-link text-muted p-0">
500 className="text-muted small"
501 title={i18n.t('number_of_comments', {
502 count: post.number_of_comments,
504 to={`/post/${post.id}`}
506 <svg class="mr-1 icon icon-inline">
507 <use xlinkHref="#icon-message-square"></use>
509 {i18n.t('number_of_comments', {
510 count: post.number_of_comments,
516 {this.state.downvotes !== 0 && (
518 class="btn text-muted py-0 pr-0"
519 data-tippy-content={this.pointsTippy}
522 <svg class="icon icon-inline mr-1">
523 <use xlinkHref="#icon-arrow-down1"></use>
525 <span>{this.state.downvotes}</span>
529 {!this.props.showBody && (
531 class="btn btn-link btn-animate text-muted py-0"
532 onClick={linkEvent(this, this.handleSavePostClick)}
534 post.saved ? i18n.t('unsave') : i18n.t('save')
539 class={`icon icon-inline ${post.saved && 'text-warning'}`}
541 <use xlinkHref="#icon-star"></use>
548 {/* This is an expanding spacer for mobile */}
549 <div className="flex-grow-1"></div>
554 className={`btn-animate btn py-0 px-1 ${
555 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
557 data-tippy-content={this.pointsTippy}
558 onClick={linkEvent(this, this.handlePostLike)}
560 <svg class="small icon icon-inline mr-2">
561 <use xlinkHref="#icon-arrow-up1"></use>
565 {this.props.enableDownvotes && (
567 className={`ml-2 btn-animate btn py-0 pl-1 ${
568 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
570 onClick={linkEvent(this, this.handlePostDisLike)}
571 data-tippy-content={this.pointsTippy}
573 <svg class="small icon icon-inline mr-2">
574 <use xlinkHref="#icon-arrow-down1"></use>
576 {this.state.downvotes !== 0 && (
577 <span>{this.state.downvotes}</span>
583 class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
584 onClick={linkEvent(this, this.handleSavePostClick)}
586 post.saved ? i18n.t('unsave') : i18n.t('save')
589 <svg class={`icon icon-inline ${post.saved && 'text-warning'}`}>
590 <use xlinkHref="#icon-star"></use>
594 {!this.state.showMoreMobile && this.props.showBody && (
596 class="btn btn-link btn-animate text-muted py-0"
597 onClick={linkEvent(this, this.handleShowMoreMobile)}
598 data-tippy-content={i18n.t('more')}
600 <svg class="icon icon-inline">
601 <use xlinkHref="#icon-more-vertical"></use>
605 {this.state.showMoreMobile && this.postActions(mobile)}
614 this.props.post.duplicates && (
615 <ul class="list-inline mb-1 small text-muted">
617 <li className="list-inline-item mr-2">
618 {i18n.t('cross_posted_to')}
620 {this.props.post.duplicates.map(post => (
621 <li className="list-inline-item mr-2">
622 <Link to={`/post/${post.id}`}>
623 {post.community_local
624 ? post.community_name
625 : `${post.community_name}@${hostname(
626 post.community_actor_id
637 postActions(mobile: boolean = false) {
638 let post = this.props.post;
640 UserService.Instance.user && (
642 {this.props.showBody && (
646 class="btn btn-link btn-animate text-muted py-0 pl-0"
647 onClick={linkEvent(this, this.handleSavePostClick)}
649 post.saved ? i18n.t('unsave') : i18n.t('save')
653 class={`icon icon-inline ${post.saved && 'text-warning'}`}
655 <use xlinkHref="#icon-star"></use>
660 className="btn btn-link btn-animate text-muted py-0"
661 to={`/create_post${this.crossPostParams}`}
662 title={i18n.t('cross_post')}
664 <svg class="icon icon-inline">
665 <use xlinkHref="#icon-copy"></use>
670 {this.myPost && this.props.showBody && (
673 class="btn btn-link btn-animate text-muted py-0"
674 onClick={linkEvent(this, this.handleEditClick)}
675 data-tippy-content={i18n.t('edit')}
677 <svg class="icon icon-inline">
678 <use xlinkHref="#icon-edit"></use>
682 class="btn btn-link btn-animate text-muted py-0"
683 onClick={linkEvent(this, this.handleDeleteClick)}
685 !post.deleted ? i18n.t('delete') : i18n.t('restore')
689 class={`icon icon-inline ${post.deleted && 'text-danger'}`}
691 <use xlinkHref="#icon-trash"></use>
697 {!this.state.showAdvanced && this.props.showBody ? (
699 class="btn btn-link btn-animate text-muted py-0"
700 onClick={linkEvent(this, this.handleShowAdvanced)}
701 data-tippy-content={i18n.t('more')}
703 <svg class="icon icon-inline">
704 <use xlinkHref="#icon-more-vertical"></use>
709 {this.props.showBody && post.body && (
711 class="btn btn-link btn-animate text-muted py-0"
712 onClick={linkEvent(this, this.handleViewSource)}
713 data-tippy-content={i18n.t('view_source')}
716 class={`icon icon-inline ${
717 this.state.viewSource && 'text-success'
720 <use xlinkHref="#icon-file-text"></use>
724 {this.canModOnSelf && (
727 class="btn btn-link btn-animate text-muted py-0"
728 onClick={linkEvent(this, this.handleModLock)}
730 post.locked ? i18n.t('unlock') : i18n.t('lock')
734 class={`icon icon-inline ${post.locked && 'text-danger'}`}
736 <use xlinkHref="#icon-lock"></use>
740 class="btn btn-link btn-animate text-muted py-0"
741 onClick={linkEvent(this, this.handleModSticky)}
743 post.stickied ? i18n.t('unsticky') : i18n.t('sticky')
747 class={`icon icon-inline ${
748 post.stickied && 'text-success'
751 <use xlinkHref="#icon-pin"></use>
756 {/* Mods can ban from community, and appoint as mods to community */}
757 {(this.canMod || this.canAdmin) &&
760 class="btn btn-link btn-animate text-muted py-0"
761 onClick={linkEvent(this, this.handleModRemoveShow)}
767 class="btn btn-link btn-animate text-muted py-0"
768 onClick={linkEvent(this, this.handleModRemoveSubmit)}
776 (!post.banned_from_community ? (
778 class="btn btn-link btn-animate text-muted py-0"
781 this.handleModBanFromCommunityShow
788 class="btn btn-link btn-animate text-muted py-0"
791 this.handleModBanFromCommunitySubmit
797 {!post.banned_from_community && post.creator_local && (
799 class="btn btn-link btn-animate text-muted py-0"
800 onClick={linkEvent(this, this.handleAddModToCommunity)}
803 ? i18n.t('remove_as_mod')
804 : i18n.t('appoint_as_mod')}
809 {/* Community creators and admins can transfer community to another mod */}
810 {(this.amCommunityCreator || this.canAdmin) &&
812 post.creator_local &&
813 (!this.state.showConfirmTransferCommunity ? (
815 class="btn btn-link btn-animate text-muted py-0"
818 this.handleShowConfirmTransferCommunity
821 {i18n.t('transfer_community')}
825 <button class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0">
826 {i18n.t('are_you_sure')}
829 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
830 onClick={linkEvent(this, this.handleTransferCommunity)}
835 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
838 this.handleCancelShowConfirmTransferCommunity
845 {/* Admins can ban from all, and appoint other admins */}
851 class="btn btn-link btn-animate text-muted py-0"
852 onClick={linkEvent(this, this.handleModBanShow)}
854 {i18n.t('ban_from_site')}
858 class="btn btn-link btn-animate text-muted py-0"
859 onClick={linkEvent(this, this.handleModBanSubmit)}
861 {i18n.t('unban_from_site')}
864 {!post.banned && post.creator_local && (
866 class="btn btn-link btn-animate text-muted py-0"
867 onClick={linkEvent(this, this.handleAddAdmin)}
870 ? i18n.t('remove_as_admin')
871 : i18n.t('appoint_as_admin')}
876 {/* Site Creator can transfer to another admin */}
877 {this.amSiteCreator &&
879 (!this.state.showConfirmTransferSite ? (
881 class="btn btn-link btn-animate text-muted py-0"
884 this.handleShowConfirmTransferSite
887 {i18n.t('transfer_site')}
891 <button class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1">
892 {i18n.t('are_you_sure')}
895 class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
896 onClick={linkEvent(this, this.handleTransferSite)}
901 class="btn btn-link btn-animate text-muted py-0 d-inline-block"
904 this.handleCancelShowConfirmTransferSite
918 removeAndBanDialogs() {
919 let post = this.props.post;
922 {this.state.showRemoveDialog && (
925 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
929 class="form-control mr-2"
930 placeholder={i18n.t('reason')}
931 value={this.state.removeReason}
932 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
934 <button type="submit" class="btn btn-secondary">
935 {i18n.t('remove_post')}
939 {this.state.showBanDialog && (
940 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
941 <div class="form-group row">
942 <label class="col-form-label" htmlFor="post-listing-reason">
947 id="post-listing-reason"
948 class="form-control mr-2"
949 placeholder={i18n.t('reason')}
950 value={this.state.banReason}
951 onInput={linkEvent(this, this.handleModBanReasonChange)}
953 <div class="form-group">
954 <div class="form-check">
956 class="form-check-input"
957 id="mod-ban-remove-data"
959 checked={this.state.removeData}
960 onChange={linkEvent(this, this.handleModRemoveDataChange)}
962 <label class="form-check-label" htmlFor="mod-ban-remove-data">
963 {i18n.t('remove_posts_comments')}
968 {/* TODO hold off on expires until later */}
969 {/* <div class="form-group row"> */}
970 {/* <label class="col-form-label">Expires</label> */}
971 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
973 <div class="form-group row">
974 <button type="submit" class="btn btn-secondary">
975 {i18n.t('ban')} {post.creator_name}
985 return this.props.post.thumbnail_url || isImage(this.props.post.url) ? (
987 <div className={`${this.state.imageExpanded ? 'col-12' : 'col-8'}`}>
988 {this.postTitleLine()}
991 {/* Post body prev or thumbnail */}
992 {!this.state.imageExpanded && this.thumbnail()}
1000 showMobilePreview() {
1002 this.props.post.body &&
1003 !this.props.showBody && (
1005 className="md-div mb-1"
1006 dangerouslySetInnerHTML={{
1007 __html: md.render(previewLines(this.props.post.body)),
1017 {/* The mobile view*/}
1018 <div class="d-block d-sm-none">
1020 <div class="col-12">
1021 {this.createdLine()}
1023 {/* If it has a thumbnail, do a right aligned thumbnail */}
1024 {this.mobileThumbnail()}
1026 {/* Show a preview of the post body */}
1027 {this.showMobilePreview()}
1029 {this.commentsLine(true)}
1030 {this.duplicatesLine()}
1031 {this.removeAndBanDialogs()}
1036 {/* The larger view*/}
1037 <div class="d-none d-sm-block">
1040 {!this.state.imageExpanded && (
1041 <div class="col-sm-2 pr-0">
1042 <div class="">{this.thumbnail()}</div>
1047 this.state.imageExpanded ? 'col-12' : 'col-12 col-sm-9'
1051 <div className="col-12">
1052 {this.postTitleLine()}
1053 {this.createdLine()}
1054 {this.commentsLine()}
1055 {this.duplicatesLine()}
1056 {this.postActions()}
1057 {this.removeAndBanDialogs()}
1067 private get myPost(): boolean {
1069 UserService.Instance.user &&
1070 this.props.post.creator_id == UserService.Instance.user.id
1074 get isMod(): boolean {
1076 this.props.moderators &&
1078 this.props.moderators.map(m => m.user_id),
1079 this.props.post.creator_id
1084 get isAdmin(): boolean {
1086 this.props.admins &&
1088 this.props.admins.map(a => a.id),
1089 this.props.post.creator_id
1094 get canMod(): boolean {
1095 if (this.props.admins && this.props.moderators) {
1096 let adminsThenMods = this.props.admins
1098 .concat(this.props.moderators.map(m => m.user_id));
1101 UserService.Instance.user,
1103 this.props.post.creator_id
1110 get canModOnSelf(): boolean {
1111 if (this.props.admins && this.props.moderators) {
1112 let adminsThenMods = this.props.admins
1114 .concat(this.props.moderators.map(m => m.user_id));
1117 UserService.Instance.user,
1119 this.props.post.creator_id,
1127 get canAdmin(): boolean {
1129 this.props.admins &&
1131 UserService.Instance.user,
1132 this.props.admins.map(a => a.id),
1133 this.props.post.creator_id
1138 get amCommunityCreator(): boolean {
1140 this.props.moderators &&
1141 UserService.Instance.user &&
1142 this.props.post.creator_id != UserService.Instance.user.id &&
1143 UserService.Instance.user.id == this.props.moderators[0].user_id
1147 get amSiteCreator(): boolean {
1149 this.props.admins &&
1150 UserService.Instance.user &&
1151 this.props.post.creator_id != UserService.Instance.user.id &&
1152 UserService.Instance.user.id == this.props.admins[0].id
1156 handlePostLike(i: PostListing) {
1157 if (!UserService.Instance.user) {
1158 this.context.router.history.push(`/login`);
1161 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1163 if (i.state.my_vote == 1) {
1166 } else if (i.state.my_vote == -1) {
1167 i.state.downvotes--;
1175 i.state.my_vote = new_vote;
1177 let form: CreatePostLikeForm = {
1178 post_id: i.props.post.id,
1179 score: i.state.my_vote,
1182 WebSocketService.Instance.likePost(form);
1183 i.setState(i.state);
1187 handlePostDisLike(i: PostListing) {
1188 if (!UserService.Instance.user) {
1189 this.context.router.history.push(`/login`);
1192 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1194 if (i.state.my_vote == 1) {
1197 i.state.downvotes++;
1198 } else if (i.state.my_vote == -1) {
1199 i.state.downvotes--;
1202 i.state.downvotes++;
1206 i.state.my_vote = new_vote;
1208 let form: CreatePostLikeForm = {
1209 post_id: i.props.post.id,
1210 score: i.state.my_vote,
1213 WebSocketService.Instance.likePost(form);
1214 i.setState(i.state);
1218 handleEditClick(i: PostListing) {
1219 i.state.showEdit = true;
1220 i.setState(i.state);
1223 handleEditCancel() {
1224 this.state.showEdit = false;
1225 this.setState(this.state);
1228 // The actual editing is done in the recieve for post
1230 this.state.showEdit = false;
1231 this.setState(this.state);
1234 handleDeleteClick(i: PostListing) {
1235 let deleteForm: DeletePostForm = {
1236 edit_id: i.props.post.id,
1237 deleted: !i.props.post.deleted,
1240 WebSocketService.Instance.deletePost(deleteForm);
1243 handleSavePostClick(i: PostListing) {
1244 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
1245 let form: SavePostForm = {
1246 post_id: i.props.post.id,
1250 WebSocketService.Instance.savePost(form);
1253 get crossPostParams(): string {
1254 let params = `?title=${this.props.post.name}`;
1255 let post = this.props.post;
1258 params += `&url=${encodeURIComponent(post.url)}`;
1260 if (this.props.post.body) {
1261 params += `&body=${this.props.post.body}`;
1266 handleModRemoveShow(i: PostListing) {
1267 i.state.showRemoveDialog = true;
1268 i.setState(i.state);
1271 handleModRemoveReasonChange(i: PostListing, event: any) {
1272 i.state.removeReason = event.target.value;
1273 i.setState(i.state);
1276 handleModRemoveDataChange(i: PostListing, event: any) {
1277 i.state.removeData = event.target.checked;
1278 i.setState(i.state);
1281 handleModRemoveSubmit(i: PostListing) {
1282 event.preventDefault();
1283 let form: RemovePostForm = {
1284 edit_id: i.props.post.id,
1285 removed: !i.props.post.removed,
1286 reason: i.state.removeReason,
1289 WebSocketService.Instance.removePost(form);
1291 i.state.showRemoveDialog = false;
1292 i.setState(i.state);
1295 handleModLock(i: PostListing) {
1296 let form: LockPostForm = {
1297 edit_id: i.props.post.id,
1298 locked: !i.props.post.locked,
1301 WebSocketService.Instance.lockPost(form);
1304 handleModSticky(i: PostListing) {
1305 let form: StickyPostForm = {
1306 edit_id: i.props.post.id,
1307 stickied: !i.props.post.stickied,
1310 WebSocketService.Instance.stickyPost(form);
1313 handleModBanFromCommunityShow(i: PostListing) {
1314 i.state.showBanDialog = true;
1315 i.state.banType = BanType.Community;
1316 i.setState(i.state);
1319 handleModBanShow(i: PostListing) {
1320 i.state.showBanDialog = true;
1321 i.state.banType = BanType.Site;
1322 i.setState(i.state);
1325 handleModBanReasonChange(i: PostListing, event: any) {
1326 i.state.banReason = event.target.value;
1327 i.setState(i.state);
1330 handleModBanExpiresChange(i: PostListing, event: any) {
1331 i.state.banExpires = event.target.value;
1332 i.setState(i.state);
1335 handleModBanFromCommunitySubmit(i: PostListing) {
1336 i.state.banType = BanType.Community;
1337 i.setState(i.state);
1338 i.handleModBanBothSubmit(i);
1341 handleModBanSubmit(i: PostListing) {
1342 i.state.banType = BanType.Site;
1343 i.setState(i.state);
1344 i.handleModBanBothSubmit(i);
1347 handleModBanBothSubmit(i: PostListing) {
1348 event.preventDefault();
1350 if (i.state.banType == BanType.Community) {
1351 // If its an unban, restore all their data
1352 let ban = !i.props.post.banned_from_community;
1354 i.state.removeData = false;
1356 let form: BanFromCommunityForm = {
1357 user_id: i.props.post.creator_id,
1358 community_id: i.props.post.community_id,
1360 remove_data: i.state.removeData,
1361 reason: i.state.banReason,
1362 expires: getUnixTime(i.state.banExpires),
1364 WebSocketService.Instance.banFromCommunity(form);
1366 // If its an unban, restore all their data
1367 let ban = !i.props.post.banned;
1369 i.state.removeData = false;
1371 let form: BanUserForm = {
1372 user_id: i.props.post.creator_id,
1374 remove_data: i.state.removeData,
1375 reason: i.state.banReason,
1376 expires: getUnixTime(i.state.banExpires),
1378 WebSocketService.Instance.banUser(form);
1381 i.state.showBanDialog = false;
1382 i.setState(i.state);
1385 handleAddModToCommunity(i: PostListing) {
1386 let form: AddModToCommunityForm = {
1387 user_id: i.props.post.creator_id,
1388 community_id: i.props.post.community_id,
1391 WebSocketService.Instance.addModToCommunity(form);
1392 i.setState(i.state);
1395 handleAddAdmin(i: PostListing) {
1396 let form: AddAdminForm = {
1397 user_id: i.props.post.creator_id,
1400 WebSocketService.Instance.addAdmin(form);
1401 i.setState(i.state);
1404 handleShowConfirmTransferCommunity(i: PostListing) {
1405 i.state.showConfirmTransferCommunity = true;
1406 i.setState(i.state);
1409 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1410 i.state.showConfirmTransferCommunity = false;
1411 i.setState(i.state);
1414 handleTransferCommunity(i: PostListing) {
1415 let form: TransferCommunityForm = {
1416 community_id: i.props.post.community_id,
1417 user_id: i.props.post.creator_id,
1419 WebSocketService.Instance.transferCommunity(form);
1420 i.state.showConfirmTransferCommunity = false;
1421 i.setState(i.state);
1424 handleShowConfirmTransferSite(i: PostListing) {
1425 i.state.showConfirmTransferSite = true;
1426 i.setState(i.state);
1429 handleCancelShowConfirmTransferSite(i: PostListing) {
1430 i.state.showConfirmTransferSite = false;
1431 i.setState(i.state);
1434 handleTransferSite(i: PostListing) {
1435 let form: TransferSiteForm = {
1436 user_id: i.props.post.creator_id,
1438 WebSocketService.Instance.transferSite(form);
1439 i.state.showConfirmTransferSite = false;
1440 i.setState(i.state);
1443 handleImageExpandClick(i: PostListing) {
1444 i.state.imageExpanded = !i.state.imageExpanded;
1445 i.setState(i.state);
1448 handleViewSource(i: PostListing) {
1449 i.state.viewSource = !i.state.viewSource;
1450 i.setState(i.state);
1453 handleShowAdvanced(i: PostListing) {
1454 i.state.showAdvanced = !i.state.showAdvanced;
1455 i.setState(i.state);
1459 handleShowMoreMobile(i: PostListing) {
1460 i.state.showMoreMobile = !i.state.showMoreMobile;
1461 i.state.showAdvanced = !i.state.showAdvanced;
1462 i.setState(i.state);
1466 get pointsTippy(): string {
1467 let points = i18n.t('number_of_points', {
1468 count: this.state.score,
1471 let upvotes = i18n.t('number_of_upvotes', {
1472 count: this.state.upvotes,
1475 let downvotes = i18n.t('number_of_downvotes', {
1476 count: this.state.downvotes,
1479 return `${points} • ${upvotes} • ${downvotes}`;