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';
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;
168 className={`img-fluid thumbnail rounded ${
169 post.nsfw || post.community_nsfw ? 'img-blur' : ''
176 getImage(thumbnail: boolean = false) {
177 let post = this.props.post;
178 if (isImage(post.url)) {
179 if (post.url.includes('pictrs')) {
180 return pictrsImage(post.url, thumbnail);
181 } else if (post.thumbnail_url) {
182 return pictrsImage(post.thumbnail_url, thumbnail);
186 } else if (post.thumbnail_url) {
187 return pictrsImage(post.thumbnail_url, thumbnail);
194 let post = this.props.post;
196 if (isImage(post.url)) {
199 class="float-right text-body pointer d-inline-block position-relative mb-2"
200 data-tippy-content={i18n.t('expand_here')}
201 onClick={linkEvent(this, this.handleImageExpandClick)}
203 {this.imgThumb(this.getImage(true))}
204 <svg class="icon mini-overlay">
205 <use xlinkHref="#icon-image"></use>
209 } else if (post.thumbnail_url) {
212 class="float-right text-body d-inline-block position-relative mb-2"
218 {this.imgThumb(this.getImage(true))}
219 <svg class="icon mini-overlay">
220 <use xlinkHref="#icon-external-link"></use>
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"
248 <div class="thumbnail rounded bg-light d-flex justify-content-center">
249 <svg class="icon d-flex align-items-center">
250 <use xlinkHref="#icon-external-link"></use>
259 className="text-body"
260 to={`/post/${post.id}`}
261 title={i18n.t('comments')}
263 <div class="thumbnail rounded bg-light d-flex justify-content-center">
264 <svg class="icon d-flex align-items-center">
265 <use xlinkHref="#icon-message-square"></use>
274 let post = this.props.post;
276 <ul class="list-inline mb-1 text-muted small">
277 <li className="list-inline-item">
280 name: post.creator_name,
281 preferred_username: post.creator_preferred_username,
282 avatar: post.creator_avatar,
284 local: post.creator_local,
285 actor_id: post.creator_actor_id,
286 published: post.creator_published,
291 <span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
294 <span className="mx-1 badge badge-light">{i18n.t('admin')}</span>
296 {(post.banned_from_community || post.banned) && (
297 <span className="mx-1 badge badge-danger">{i18n.t('banned')}</span>
299 {this.props.showCommunity && (
301 <span class="mx-1"> {i18n.t('to')} </span>
304 name: post.community_name,
305 id: post.community_id,
306 local: post.community_local,
307 actor_id: post.community_actor_id,
308 icon: post.community_icon,
314 <li className="list-inline-item">•</li>
315 {post.url && !(hostname(post.url) == externalHost) && (
317 <li className="list-inline-item">
319 className="text-muted font-italic"
328 <li className="list-inline-item">•</li>
331 <li className="list-inline-item">
333 <MomentTime data={post} />
338 <li className="list-inline-item">•</li>
339 <li className="list-inline-item">
340 {/* Using a link with tippy doesn't work on touch devices unfortunately */}
342 className="text-muted"
343 data-tippy-content={md.render(previewLines(post.body))}
344 data-tippy-allowHtml={true}
345 to={`/post/${post.id}`}
347 <svg class="mr-1 icon icon-inline">
348 <use xlinkHref="#icon-book-open"></use>
360 <div className={`vote-bar col-1 pr-0 small text-center`}>
362 className={`btn-animate btn btn-link p-0 ${
363 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
365 onClick={linkEvent(this, this.handlePostLike)}
366 data-tippy-content={i18n.t('upvote')}
368 <svg class="icon upvote">
369 <use xlinkHref="#icon-arrow-up1"></use>
373 class={`unselectable pointer font-weight-bold text-muted px-1`}
374 data-tippy-content={this.pointsTippy}
378 {this.props.enableDownvotes && (
380 className={`btn-animate btn btn-link p-0 ${
381 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
383 onClick={linkEvent(this, this.handlePostDisLike)}
384 data-tippy-content={i18n.t('downvote')}
386 <svg class="icon downvote">
387 <use xlinkHref="#icon-arrow-down1"></use>
396 let post = this.props.post;
398 <div className="post-title overflow-hidden">
400 {this.props.showBody && post.url ? (
402 className={!post.stickied ? 'text-body' : 'text-primary'}
412 className={!post.stickied ? 'text-body' : 'text-primary'}
413 to={`/post/${post.id}`}
414 title={i18n.t('comments')}
419 {(isImage(post.url) || this.props.post.thumbnail_url) &&
420 (!this.state.imageExpanded ? (
422 class="text-monospace unselectable pointer ml-2 text-muted small"
423 data-tippy-content={i18n.t('expand_here')}
424 onClick={linkEvent(this, this.handleImageExpandClick)}
426 <svg class="icon icon-inline">
427 <use xlinkHref="#icon-plus-square"></use>
433 class="text-monospace unselectable pointer ml-2 text-muted small"
434 onClick={linkEvent(this, this.handleImageExpandClick)}
436 <svg class="icon icon-inline">
437 <use xlinkHref="#icon-minus-square"></use>
443 onClick={linkEvent(this, this.handleImageExpandClick)}
445 <img class="img-fluid img-expanded" src={this.getImage()} />
451 <small className="ml-2 text-muted font-italic">
457 className="unselectable pointer ml-2 text-muted font-italic"
458 data-tippy-content={i18n.t('deleted')}
460 <svg class={`icon icon-inline text-danger`}>
461 <use xlinkHref="#icon-trash"></use>
467 className="unselectable pointer ml-2 text-muted font-italic"
468 data-tippy-content={i18n.t('locked')}
470 <svg class={`icon icon-inline text-danger`}>
471 <use xlinkHref="#icon-lock"></use>
477 className="unselectable pointer ml-2 text-muted font-italic"
478 data-tippy-content={i18n.t('stickied')}
480 <svg class={`icon icon-inline text-primary`}>
481 <use xlinkHref="#icon-pin"></use>
486 <small className="ml-2 text-muted font-italic">
495 commentsLine(mobile: boolean = false) {
496 let post = this.props.post;
498 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
499 <button class="btn btn-link text-muted p-0">
501 className="text-muted small"
502 title={i18n.t('number_of_comments', {
503 count: post.number_of_comments,
505 to={`/post/${post.id}`}
507 <svg class="mr-1 icon icon-inline">
508 <use xlinkHref="#icon-message-square"></use>
510 {i18n.t('number_of_comments', {
511 count: post.number_of_comments,
517 {this.state.downvotes !== 0 && (
519 class="btn text-muted py-0 pr-0"
520 data-tippy-content={this.pointsTippy}
523 <svg class="icon icon-inline mr-1">
524 <use xlinkHref="#icon-arrow-down1"></use>
526 <span>{this.state.downvotes}</span>
530 {!this.props.showBody && (
532 class="btn btn-link btn-animate text-muted py-0"
533 onClick={linkEvent(this, this.handleSavePostClick)}
535 post.saved ? i18n.t('unsave') : i18n.t('save')
540 class={`icon icon-inline ${post.saved && 'text-warning'}`}
542 <use xlinkHref="#icon-star"></use>
549 {/* This is an expanding spacer for mobile */}
550 <div className="flex-grow-1"></div>
555 className={`btn-animate btn py-0 px-1 ${
556 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
558 data-tippy-content={this.pointsTippy}
559 onClick={linkEvent(this, this.handlePostLike)}
561 <svg class="small icon icon-inline mr-2">
562 <use xlinkHref="#icon-arrow-up1"></use>
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>
582 class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
583 onClick={linkEvent(this, this.handleSavePostClick)}
585 post.saved ? i18n.t('unsave') : i18n.t('save')
588 <svg class={`icon icon-inline ${post.saved && 'text-warning'}`}>
589 <use xlinkHref="#icon-star"></use>
593 {!this.state.showMoreMobile && this.props.showBody && (
595 class="btn btn-link btn-animate text-muted py-0"
596 onClick={linkEvent(this, this.handleShowMoreMobile)}
597 data-tippy-content={i18n.t('more')}
599 <svg class="icon icon-inline">
600 <use xlinkHref="#icon-more-vertical"></use>
604 {this.state.showMoreMobile && this.postActions(mobile)}
613 this.props.post.duplicates && (
614 <ul class="list-inline mb-1 small text-muted">
616 <li className="list-inline-item mr-2">
617 {i18n.t('cross_posted_to')}
619 {this.props.post.duplicates.map(post => (
620 <li className="list-inline-item mr-2">
621 <Link to={`/post/${post.id}`}>{post.community_name}</Link>
630 postActions(mobile: boolean = false) {
631 let post = this.props.post;
633 UserService.Instance.user && (
635 {this.props.showBody && (
639 class="btn btn-link btn-animate text-muted py-0 pl-0"
640 onClick={linkEvent(this, this.handleSavePostClick)}
642 post.saved ? i18n.t('unsave') : i18n.t('save')
646 class={`icon icon-inline ${post.saved && 'text-warning'}`}
648 <use xlinkHref="#icon-star"></use>
653 className="btn btn-link btn-animate text-muted py-0"
654 to={`/create_post${this.crossPostParams}`}
655 title={i18n.t('cross_post')}
657 <svg class="icon icon-inline">
658 <use xlinkHref="#icon-copy"></use>
663 {this.myPost && this.props.showBody && (
666 class="btn btn-link btn-animate text-muted py-0"
667 onClick={linkEvent(this, this.handleEditClick)}
668 data-tippy-content={i18n.t('edit')}
670 <svg class="icon icon-inline">
671 <use xlinkHref="#icon-edit"></use>
675 class="btn btn-link btn-animate text-muted py-0"
676 onClick={linkEvent(this, this.handleDeleteClick)}
678 !post.deleted ? i18n.t('delete') : i18n.t('restore')
682 class={`icon icon-inline ${post.deleted && 'text-danger'}`}
684 <use xlinkHref="#icon-trash"></use>
690 {!this.state.showAdvanced && this.props.showBody ? (
692 class="btn btn-link btn-animate text-muted py-0"
693 onClick={linkEvent(this, this.handleShowAdvanced)}
694 data-tippy-content={i18n.t('more')}
696 <svg class="icon icon-inline">
697 <use xlinkHref="#icon-more-vertical"></use>
702 {this.props.showBody && post.body && (
704 class="btn btn-link btn-animate text-muted py-0"
705 onClick={linkEvent(this, this.handleViewSource)}
706 data-tippy-content={i18n.t('view_source')}
709 class={`icon icon-inline ${
710 this.state.viewSource && 'text-success'
713 <use xlinkHref="#icon-file-text"></use>
717 {this.canModOnSelf && (
720 class="btn btn-link btn-animate text-muted py-0"
721 onClick={linkEvent(this, this.handleModLock)}
723 post.locked ? i18n.t('unlock') : i18n.t('lock')
727 class={`icon icon-inline ${post.locked && 'text-danger'}`}
729 <use xlinkHref="#icon-lock"></use>
733 class="btn btn-link btn-animate text-muted py-0"
734 onClick={linkEvent(this, this.handleModSticky)}
736 post.stickied ? i18n.t('unsticky') : i18n.t('sticky')
740 class={`icon icon-inline ${
741 post.stickied && 'text-success'
744 <use xlinkHref="#icon-pin"></use>
749 {/* Mods can ban from community, and appoint as mods to community */}
750 {(this.canMod || this.canAdmin) &&
754 onClick={linkEvent(this, this.handleModRemoveShow)}
761 onClick={linkEvent(this, this.handleModRemoveSubmit)}
769 (!post.banned_from_community ? (
774 this.handleModBanFromCommunityShow
784 this.handleModBanFromCommunitySubmit
790 {!post.banned_from_community && post.creator_local && (
793 onClick={linkEvent(this, this.handleAddModToCommunity)}
796 ? i18n.t('remove_as_mod')
797 : i18n.t('appoint_as_mod')}
802 {/* Community creators and admins can transfer community to another mod */}
803 {(this.amCommunityCreator || this.canAdmin) &&
805 post.creator_local &&
806 (!this.state.showConfirmTransferCommunity ? (
811 this.handleShowConfirmTransferCommunity
814 {i18n.t('transfer_community')}
818 <span class="d-inline-block mr-1">
819 {i18n.t('are_you_sure')}
822 class="pointer d-inline-block mr-1"
823 onClick={linkEvent(this, this.handleTransferCommunity)}
828 class="pointer d-inline-block"
831 this.handleCancelShowConfirmTransferCommunity
838 {/* Admins can ban from all, and appoint other admins */}
845 onClick={linkEvent(this, this.handleModBanShow)}
847 {i18n.t('ban_from_site')}
852 onClick={linkEvent(this, this.handleModBanSubmit)}
854 {i18n.t('unban_from_site')}
857 {!post.banned && post.creator_local && (
860 onClick={linkEvent(this, this.handleAddAdmin)}
863 ? i18n.t('remove_as_admin')
864 : i18n.t('appoint_as_admin')}
869 {/* Site Creator can transfer to another admin */}
870 {this.amSiteCreator &&
872 (!this.state.showConfirmTransferSite ? (
877 this.handleShowConfirmTransferSite
880 {i18n.t('transfer_site')}
884 <span class="d-inline-block mr-1">
885 {i18n.t('are_you_sure')}
888 class="pointer d-inline-block mr-1"
889 onClick={linkEvent(this, this.handleTransferSite)}
894 class="pointer d-inline-block"
897 this.handleCancelShowConfirmTransferSite
911 removeAndBanDialogs() {
912 let post = this.props.post;
915 {this.state.showRemoveDialog && (
918 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
922 class="form-control mr-2"
923 placeholder={i18n.t('reason')}
924 value={this.state.removeReason}
925 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
927 <button type="submit" class="btn btn-secondary">
928 {i18n.t('remove_post')}
932 {this.state.showBanDialog && (
933 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
934 <div class="form-group row">
935 <label class="col-form-label" htmlFor="post-listing-reason">
940 id="post-listing-reason"
941 class="form-control mr-2"
942 placeholder={i18n.t('reason')}
943 value={this.state.banReason}
944 onInput={linkEvent(this, this.handleModBanReasonChange)}
946 <div class="form-group">
947 <div class="form-check">
949 class="form-check-input"
950 id="mod-ban-remove-data"
952 checked={this.state.removeData}
953 onChange={linkEvent(this, this.handleModRemoveDataChange)}
955 <label class="form-check-label" htmlFor="mod-ban-remove-data">
956 {i18n.t('remove_posts_comments')}
961 {/* TODO hold off on expires until later */}
962 {/* <div class="form-group row"> */}
963 {/* <label class="col-form-label">Expires</label> */}
964 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
966 <div class="form-group row">
967 <button type="submit" class="btn btn-secondary">
968 {i18n.t('ban')} {post.creator_name}
978 return this.props.post.thumbnail_url || isImage(this.props.post.url) ? (
980 <div className={`${this.state.imageExpanded ? 'col-12' : 'col-8'}`}>
981 {this.postTitleLine()}
984 {/* Post body prev or thumbnail */}
985 {!this.state.imageExpanded && this.thumbnail()}
993 showMobilePreview() {
995 this.props.post.body &&
996 !this.props.showBody && (
998 className="md-div mb-1"
999 dangerouslySetInnerHTML={{
1000 __html: md.render(previewLines(this.props.post.body)),
1010 {/* The mobile view*/}
1011 <div class="d-block d-sm-none">
1013 <div class="col-12">
1014 {this.createdLine()}
1016 {/* If it has a thumbnail, do a right aligned thumbnail */}
1017 {this.mobileThumbnail()}
1019 {/* Show a preview of the post body */}
1020 {this.showMobilePreview()}
1022 {this.commentsLine(true)}
1023 {this.duplicatesLine()}
1024 {this.removeAndBanDialogs()}
1029 {/* The larger view*/}
1030 <div class="d-none d-sm-block">
1033 {!this.state.imageExpanded && (
1034 <div class="col-sm-2 pr-0">
1035 <div class="">{this.thumbnail()}</div>
1040 this.state.imageExpanded ? 'col-12' : 'col-12 col-sm-9'
1044 <div className="col-12">
1045 {this.postTitleLine()}
1046 {this.createdLine()}
1047 {this.commentsLine()}
1048 {this.duplicatesLine()}
1049 {this.postActions()}
1050 {this.removeAndBanDialogs()}
1060 private get myPost(): boolean {
1062 UserService.Instance.user &&
1063 this.props.post.creator_id == UserService.Instance.user.id
1067 get isMod(): boolean {
1069 this.props.moderators &&
1071 this.props.moderators.map(m => m.user_id),
1072 this.props.post.creator_id
1077 get isAdmin(): boolean {
1079 this.props.admins &&
1081 this.props.admins.map(a => a.id),
1082 this.props.post.creator_id
1087 get canMod(): boolean {
1088 if (this.props.admins && this.props.moderators) {
1089 let adminsThenMods = this.props.admins
1091 .concat(this.props.moderators.map(m => m.user_id));
1094 UserService.Instance.user,
1096 this.props.post.creator_id
1103 get canModOnSelf(): boolean {
1104 if (this.props.admins && this.props.moderators) {
1105 let adminsThenMods = this.props.admins
1107 .concat(this.props.moderators.map(m => m.user_id));
1110 UserService.Instance.user,
1112 this.props.post.creator_id,
1120 get canAdmin(): boolean {
1122 this.props.admins &&
1124 UserService.Instance.user,
1125 this.props.admins.map(a => a.id),
1126 this.props.post.creator_id
1131 get amCommunityCreator(): boolean {
1133 this.props.moderators &&
1134 UserService.Instance.user &&
1135 this.props.post.creator_id != UserService.Instance.user.id &&
1136 UserService.Instance.user.id == this.props.moderators[0].user_id
1140 get amSiteCreator(): boolean {
1142 this.props.admins &&
1143 UserService.Instance.user &&
1144 this.props.post.creator_id != UserService.Instance.user.id &&
1145 UserService.Instance.user.id == this.props.admins[0].id
1149 handlePostLike(i: PostListing) {
1150 if (!UserService.Instance.user) {
1151 this.context.router.history.push(`/login`);
1154 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1156 if (i.state.my_vote == 1) {
1159 } else if (i.state.my_vote == -1) {
1160 i.state.downvotes--;
1168 i.state.my_vote = new_vote;
1170 let form: CreatePostLikeForm = {
1171 post_id: i.props.post.id,
1172 score: i.state.my_vote,
1175 WebSocketService.Instance.likePost(form);
1176 i.setState(i.state);
1180 handlePostDisLike(i: PostListing) {
1181 if (!UserService.Instance.user) {
1182 this.context.router.history.push(`/login`);
1185 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1187 if (i.state.my_vote == 1) {
1190 i.state.downvotes++;
1191 } else if (i.state.my_vote == -1) {
1192 i.state.downvotes--;
1195 i.state.downvotes++;
1199 i.state.my_vote = new_vote;
1201 let form: CreatePostLikeForm = {
1202 post_id: i.props.post.id,
1203 score: i.state.my_vote,
1206 WebSocketService.Instance.likePost(form);
1207 i.setState(i.state);
1211 handleEditClick(i: PostListing) {
1212 i.state.showEdit = true;
1213 i.setState(i.state);
1216 handleEditCancel() {
1217 this.state.showEdit = false;
1218 this.setState(this.state);
1221 // The actual editing is done in the recieve for post
1223 this.state.showEdit = false;
1224 this.setState(this.state);
1227 handleDeleteClick(i: PostListing) {
1228 let deleteForm: DeletePostForm = {
1229 edit_id: i.props.post.id,
1230 deleted: !i.props.post.deleted,
1233 WebSocketService.Instance.deletePost(deleteForm);
1236 handleSavePostClick(i: PostListing) {
1237 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
1238 let form: SavePostForm = {
1239 post_id: i.props.post.id,
1243 WebSocketService.Instance.savePost(form);
1246 get crossPostParams(): string {
1247 let params = `?title=${this.props.post.name}`;
1248 let post = this.props.post;
1251 params += `&url=${encodeURIComponent(post.url)}`;
1253 if (this.props.post.body) {
1254 params += `&body=${this.props.post.body}`;
1259 handleModRemoveShow(i: PostListing) {
1260 i.state.showRemoveDialog = true;
1261 i.setState(i.state);
1264 handleModRemoveReasonChange(i: PostListing, event: any) {
1265 i.state.removeReason = event.target.value;
1266 i.setState(i.state);
1269 handleModRemoveDataChange(i: PostListing, event: any) {
1270 i.state.removeData = event.target.checked;
1271 i.setState(i.state);
1274 handleModRemoveSubmit(i: PostListing) {
1275 event.preventDefault();
1276 let form: RemovePostForm = {
1277 edit_id: i.props.post.id,
1278 removed: !i.props.post.removed,
1279 reason: i.state.removeReason,
1282 WebSocketService.Instance.removePost(form);
1284 i.state.showRemoveDialog = false;
1285 i.setState(i.state);
1288 handleModLock(i: PostListing) {
1289 let form: LockPostForm = {
1290 edit_id: i.props.post.id,
1291 locked: !i.props.post.locked,
1294 WebSocketService.Instance.lockPost(form);
1297 handleModSticky(i: PostListing) {
1298 let form: StickyPostForm = {
1299 edit_id: i.props.post.id,
1300 stickied: !i.props.post.stickied,
1303 WebSocketService.Instance.stickyPost(form);
1306 handleModBanFromCommunityShow(i: PostListing) {
1307 i.state.showBanDialog = true;
1308 i.state.banType = BanType.Community;
1309 i.setState(i.state);
1312 handleModBanShow(i: PostListing) {
1313 i.state.showBanDialog = true;
1314 i.state.banType = BanType.Site;
1315 i.setState(i.state);
1318 handleModBanReasonChange(i: PostListing, event: any) {
1319 i.state.banReason = event.target.value;
1320 i.setState(i.state);
1323 handleModBanExpiresChange(i: PostListing, event: any) {
1324 i.state.banExpires = event.target.value;
1325 i.setState(i.state);
1328 handleModBanFromCommunitySubmit(i: PostListing) {
1329 i.state.banType = BanType.Community;
1330 i.setState(i.state);
1331 i.handleModBanBothSubmit(i);
1334 handleModBanSubmit(i: PostListing) {
1335 i.state.banType = BanType.Site;
1336 i.setState(i.state);
1337 i.handleModBanBothSubmit(i);
1340 handleModBanBothSubmit(i: PostListing) {
1341 event.preventDefault();
1343 if (i.state.banType == BanType.Community) {
1344 // If its an unban, restore all their data
1345 let ban = !i.props.post.banned_from_community;
1347 i.state.removeData = false;
1349 let form: BanFromCommunityForm = {
1350 user_id: i.props.post.creator_id,
1351 community_id: i.props.post.community_id,
1353 remove_data: i.state.removeData,
1354 reason: i.state.banReason,
1355 expires: getUnixTime(i.state.banExpires),
1357 WebSocketService.Instance.banFromCommunity(form);
1359 // If its an unban, restore all their data
1360 let ban = !i.props.post.banned;
1362 i.state.removeData = false;
1364 let form: BanUserForm = {
1365 user_id: i.props.post.creator_id,
1367 remove_data: i.state.removeData,
1368 reason: i.state.banReason,
1369 expires: getUnixTime(i.state.banExpires),
1371 WebSocketService.Instance.banUser(form);
1374 i.state.showBanDialog = false;
1375 i.setState(i.state);
1378 handleAddModToCommunity(i: PostListing) {
1379 let form: AddModToCommunityForm = {
1380 user_id: i.props.post.creator_id,
1381 community_id: i.props.post.community_id,
1384 WebSocketService.Instance.addModToCommunity(form);
1385 i.setState(i.state);
1388 handleAddAdmin(i: PostListing) {
1389 let form: AddAdminForm = {
1390 user_id: i.props.post.creator_id,
1393 WebSocketService.Instance.addAdmin(form);
1394 i.setState(i.state);
1397 handleShowConfirmTransferCommunity(i: PostListing) {
1398 i.state.showConfirmTransferCommunity = true;
1399 i.setState(i.state);
1402 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1403 i.state.showConfirmTransferCommunity = false;
1404 i.setState(i.state);
1407 handleTransferCommunity(i: PostListing) {
1408 let form: TransferCommunityForm = {
1409 community_id: i.props.post.community_id,
1410 user_id: i.props.post.creator_id,
1412 WebSocketService.Instance.transferCommunity(form);
1413 i.state.showConfirmTransferCommunity = false;
1414 i.setState(i.state);
1417 handleShowConfirmTransferSite(i: PostListing) {
1418 i.state.showConfirmTransferSite = true;
1419 i.setState(i.state);
1422 handleCancelShowConfirmTransferSite(i: PostListing) {
1423 i.state.showConfirmTransferSite = false;
1424 i.setState(i.state);
1427 handleTransferSite(i: PostListing) {
1428 let form: TransferSiteForm = {
1429 user_id: i.props.post.creator_id,
1431 WebSocketService.Instance.transferSite(form);
1432 i.state.showConfirmTransferSite = false;
1433 i.setState(i.state);
1436 handleImageExpandClick(i: PostListing) {
1437 i.state.imageExpanded = !i.state.imageExpanded;
1438 i.setState(i.state);
1441 handleViewSource(i: PostListing) {
1442 i.state.viewSource = !i.state.viewSource;
1443 i.setState(i.state);
1446 handleShowAdvanced(i: PostListing) {
1447 i.state.showAdvanced = !i.state.showAdvanced;
1448 i.setState(i.state);
1452 handleShowMoreMobile(i: PostListing) {
1453 i.state.showMoreMobile = !i.state.showMoreMobile;
1454 i.state.showAdvanced = !i.state.showAdvanced;
1455 i.setState(i.state);
1459 get pointsTippy(): string {
1460 let points = i18n.t('number_of_points', {
1461 count: this.state.score,
1464 let upvotes = i18n.t('number_of_upvotes', {
1465 count: this.state.upvotes,
1468 let downvotes = i18n.t('number_of_downvotes', {
1469 count: this.state.downvotes,
1472 return `${points} • ${upvotes} • ${downvotes}`;