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 { UserListing } from './user-listing';
26 import { CommunityLink } from './community-link';
27 import { PictrsImage } from './pictrs-image';
42 import { i18n } from '../i18next';
43 import { externalHost } from '../env';
45 interface PostListingState {
47 showRemoveDialog: boolean;
49 showBanDialog: boolean;
54 showConfirmTransferSite: boolean;
55 showConfirmTransferCommunity: boolean;
56 imageExpanded: boolean;
58 showAdvanced: boolean;
59 showMoreMobile: boolean;
66 interface PostListingProps {
68 duplicates?: PostView[];
69 showCommunity?: boolean;
71 moderators?: CommunityModeratorView[];
72 admins?: UserViewSafe[];
73 enableDownvotes: boolean;
77 export class PostListing extends Component<PostListingProps, PostListingState> {
78 private emptyState: PostListingState = {
80 showRemoveDialog: false,
86 banType: BanType.Community,
87 showConfirmTransferSite: false,
88 showConfirmTransferCommunity: false,
92 showMoreMobile: false,
93 my_vote: this.props.post_view.my_vote,
94 score: this.props.post_view.counts.score,
95 upvotes: this.props.post_view.counts.upvotes,
96 downvotes: this.props.post_view.counts.downvotes,
99 constructor(props: any, context: any) {
100 super(props, context);
102 this.state = this.emptyState;
103 this.handlePostLike = this.handlePostLike.bind(this);
104 this.handlePostDisLike = this.handlePostDisLike.bind(this);
105 this.handleEditPost = this.handleEditPost.bind(this);
106 this.handleEditCancel = this.handleEditCancel.bind(this);
109 componentWillReceiveProps(nextProps: PostListingProps) {
110 this.state.my_vote = nextProps.post_view.my_vote;
111 this.state.upvotes = nextProps.post_view.counts.upvotes;
112 this.state.downvotes = nextProps.post_view.counts.downvotes;
113 this.state.score = nextProps.post_view.counts.score;
114 if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
115 this.state.imageExpanded = false;
117 this.setState(this.state);
123 {!this.state.showEdit ? (
131 post_view={this.props.post_view}
132 onEdit={this.handleEditPost}
133 onCancel={this.handleEditCancel}
134 enableNsfw={this.props.enableNsfw}
135 enableDownvotes={this.props.enableDownvotes}
144 let post = this.props.post_view.post;
148 {post.url && this.props.showBody && post.embed_title && (
149 <IFramelyCard post={post} />
151 {this.props.showBody &&
153 (this.state.viewSource ? (
154 <pre>{post.body}</pre>
158 dangerouslySetInnerHTML={mdToHtml(post.body)}
166 imgThumb(src: string) {
167 let post_view = this.props.post_view;
172 nsfw={post_view.post.nsfw || post_view.community.nsfw}
177 getImageSrc(): string {
178 let post = this.props.post_view.post;
179 if (isImage(post.url)) {
180 if (post.url.includes('pictrs')) {
182 } else if (post.thumbnail_url) {
183 return post.thumbnail_url;
187 } else if (post.thumbnail_url) {
188 return post.thumbnail_url;
195 let post = this.props.post_view.post;
197 if (isImage(post.url)) {
200 class="float-right text-body pointer d-inline-block position-relative mb-2"
201 data-tippy-content={i18n.t('expand_here')}
202 onClick={linkEvent(this, this.handleImageExpandClick)}
204 {this.imgThumb(this.getImageSrc())}
205 <svg class="icon mini-overlay">
206 <use xlinkHref="#icon-image"></use>
210 } else if (post.thumbnail_url) {
213 class="float-right text-body d-inline-block position-relative mb-2"
219 {this.imgThumb(this.getImageSrc())}
220 <svg class="icon mini-overlay">
221 <use xlinkHref="#icon-external-link"></use>
225 } else if (post.url) {
226 if (isVideo(post.url)) {
228 <div class="embed-responsive embed-responsive-16by9">
234 class="embed-responsive-item"
236 <source src={post.url} type="video/mp4" />
243 className="text-body"
249 <div class="thumbnail rounded bg-light d-flex justify-content-center">
250 <svg class="icon d-flex align-items-center">
251 <use xlinkHref="#icon-external-link"></use>
260 className="text-body"
261 to={`/post/${post.id}`}
262 title={i18n.t('comments')}
264 <div class="thumbnail rounded bg-light d-flex justify-content-center">
265 <svg class="icon d-flex align-items-center">
266 <use xlinkHref="#icon-message-square"></use>
275 let post_view = this.props.post_view;
277 <ul class="list-inline mb-1 text-muted small">
278 <li className="list-inline-item">
279 <UserListing user={post_view.creator} />
282 <span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
285 <span className="mx-1 badge badge-light">{i18n.t('admin')}</span>
287 {(post_view.creator_banned_from_community ||
288 post_view.creator.banned) && (
289 <span className="mx-1 badge badge-danger">{i18n.t('banned')}</span>
291 {this.props.showCommunity && (
293 <span class="mx-1"> {i18n.t('to')} </span>
294 <CommunityLink community={post_view.community} />
298 <li className="list-inline-item">•</li>
299 {post_view.post.url && !(hostname(post_view.post.url) == externalHost) && (
301 <li className="list-inline-item">
303 className="text-muted font-italic"
304 href={post_view.post.url}
306 title={post_view.post.url}
309 {hostname(post_view.post.url)}
312 <li className="list-inline-item">•</li>
315 <li className="list-inline-item">
317 <MomentTime data={post_view.post} />
320 {post_view.post.body && (
322 <li className="list-inline-item">•</li>
323 <li className="list-inline-item">
324 {/* Using a link with tippy doesn't work on touch devices unfortunately */}
326 className="text-muted"
327 data-tippy-content={md.render(
328 previewLines(post_view.post.body)
330 data-tippy-allowHtml={true}
331 to={`/post/${post_view.post.id}`}
333 <svg class="mr-1 icon icon-inline">
334 <use xlinkHref="#icon-book-open"></use>
346 <div className={`vote-bar col-1 pr-0 small text-center`}>
348 className={`btn-animate btn btn-link p-0 ${
349 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
351 onClick={linkEvent(this, this.handlePostLike)}
352 data-tippy-content={i18n.t('upvote')}
354 <svg class="icon upvote">
355 <use xlinkHref="#icon-arrow-up1"></use>
359 class={`unselectable pointer font-weight-bold text-muted px-1`}
360 data-tippy-content={this.pointsTippy}
364 {this.props.enableDownvotes && (
366 className={`btn-animate btn btn-link p-0 ${
367 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
369 onClick={linkEvent(this, this.handlePostDisLike)}
370 data-tippy-content={i18n.t('downvote')}
372 <svg class="icon downvote">
373 <use xlinkHref="#icon-arrow-down1"></use>
382 let post = this.props.post_view.post;
384 <div className="post-title overflow-hidden">
386 {this.props.showBody && post.url ? (
388 className={!post.stickied ? 'text-body' : 'text-primary'}
398 className={!post.stickied ? 'text-body' : 'text-primary'}
399 to={`/post/${post.id}`}
400 title={i18n.t('comments')}
405 {(isImage(post.url) || post.thumbnail_url) &&
406 (!this.state.imageExpanded ? (
408 class="text-monospace unselectable pointer ml-2 text-muted small"
409 data-tippy-content={i18n.t('expand_here')}
410 onClick={linkEvent(this, this.handleImageExpandClick)}
412 <svg class="icon icon-inline">
413 <use xlinkHref="#icon-plus-square"></use>
419 class="text-monospace unselectable pointer ml-2 text-muted small"
420 onClick={linkEvent(this, this.handleImageExpandClick)}
422 <svg class="icon icon-inline">
423 <use xlinkHref="#icon-minus-square"></use>
429 onClick={linkEvent(this, this.handleImageExpandClick)}
431 <PictrsImage src={this.getImageSrc()} />
437 <small className="ml-2 text-muted font-italic">
443 className="unselectable pointer ml-2 text-muted font-italic"
444 data-tippy-content={i18n.t('deleted')}
446 <svg class={`icon icon-inline text-danger`}>
447 <use xlinkHref="#icon-trash"></use>
453 className="unselectable pointer ml-2 text-muted font-italic"
454 data-tippy-content={i18n.t('locked')}
456 <svg class={`icon icon-inline text-danger`}>
457 <use xlinkHref="#icon-lock"></use>
463 className="unselectable pointer ml-2 text-muted font-italic"
464 data-tippy-content={i18n.t('stickied')}
466 <svg class={`icon icon-inline text-primary`}>
467 <use xlinkHref="#icon-pin"></use>
472 <small className="ml-2 text-muted font-italic">
481 commentsLine(mobile: boolean = false) {
482 let post_view = this.props.post_view;
484 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
485 <button class="btn btn-link text-muted p-0">
487 className="text-muted small"
488 title={i18n.t('number_of_comments', {
489 count: post_view.counts.comments,
491 to={`/post/${post_view.post.id}`}
493 <svg class="mr-1 icon icon-inline">
494 <use xlinkHref="#icon-message-square"></use>
496 {i18n.t('number_of_comments', {
497 count: post_view.counts.comments,
503 {this.state.downvotes !== 0 && (
505 class="btn text-muted py-0 pr-0"
506 data-tippy-content={this.pointsTippy}
509 <svg class="icon icon-inline mr-1">
510 <use xlinkHref="#icon-arrow-down1"></use>
512 <span>{this.state.downvotes}</span>
516 {!this.props.showBody && (
518 class="btn btn-link btn-animate text-muted py-0"
519 onClick={linkEvent(this, this.handleSavePostClick)}
521 post_view.saved ? i18n.t('unsave') : i18n.t('save')
526 class={`icon icon-inline ${
527 post_view.saved && 'text-warning'
530 <use xlinkHref="#icon-star"></use>
537 {/* This is an expanding spacer for mobile */}
538 <div className="flex-grow-1"></div>
543 className={`btn-animate btn py-0 px-1 ${
544 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
546 data-tippy-content={this.pointsTippy}
547 onClick={linkEvent(this, this.handlePostLike)}
549 <svg class="small icon icon-inline mr-2">
550 <use xlinkHref="#icon-arrow-up1"></use>
554 {this.props.enableDownvotes && (
556 className={`ml-2 btn-animate btn py-0 pl-1 ${
557 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
559 onClick={linkEvent(this, this.handlePostDisLike)}
560 data-tippy-content={this.pointsTippy}
562 <svg class="small icon icon-inline mr-2">
563 <use xlinkHref="#icon-arrow-down1"></use>
565 {this.state.downvotes !== 0 && (
566 <span>{this.state.downvotes}</span>
572 class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
573 onClick={linkEvent(this, this.handleSavePostClick)}
575 post_view.saved ? i18n.t('unsave') : i18n.t('save')
579 class={`icon icon-inline ${post_view.saved && 'text-warning'}`}
581 <use xlinkHref="#icon-star"></use>
585 {!this.state.showMoreMobile && this.props.showBody && (
587 class="btn btn-link btn-animate text-muted py-0"
588 onClick={linkEvent(this, this.handleShowMoreMobile)}
589 data-tippy-content={i18n.t('more')}
591 <svg class="icon icon-inline">
592 <use xlinkHref="#icon-more-vertical"></use>
596 {this.state.showMoreMobile && this.postActions(mobile)}
604 let dupes = this.props.duplicates;
607 dupes.length > 0 && (
608 <ul class="list-inline mb-1 small text-muted">
610 <li className="list-inline-item mr-2">
611 {i18n.t('cross_posted_to')}
614 <li className="list-inline-item mr-2">
615 <Link to={`/post/${pv.post.id}`}>
618 : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
628 postActions(mobile: boolean = false) {
629 let post_view = this.props.post_view;
631 UserService.Instance.user && (
633 {this.props.showBody && (
637 class="btn btn-link btn-animate text-muted py-0 pl-0"
638 onClick={linkEvent(this, this.handleSavePostClick)}
640 post_view.saved ? i18n.t('unsave') : i18n.t('save')
644 class={`icon icon-inline ${
645 post_view.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_view.post.deleted ? i18n.t('delete') : i18n.t('restore')
682 class={`icon icon-inline ${
683 post_view.post.deleted && 'text-danger'
686 <use xlinkHref="#icon-trash"></use>
692 {!this.state.showAdvanced && this.props.showBody ? (
694 class="btn btn-link btn-animate text-muted py-0"
695 onClick={linkEvent(this, this.handleShowAdvanced)}
696 data-tippy-content={i18n.t('more')}
698 <svg class="icon icon-inline">
699 <use xlinkHref="#icon-more-vertical"></use>
704 {this.props.showBody && post_view.post.body && (
706 class="btn btn-link btn-animate text-muted py-0"
707 onClick={linkEvent(this, this.handleViewSource)}
708 data-tippy-content={i18n.t('view_source')}
711 class={`icon icon-inline ${
712 this.state.viewSource && 'text-success'
715 <use xlinkHref="#icon-file-text"></use>
719 {this.canModOnSelf && (
722 class="btn btn-link btn-animate text-muted py-0"
723 onClick={linkEvent(this, this.handleModLock)}
725 post_view.post.locked ? i18n.t('unlock') : i18n.t('lock')
729 class={`icon icon-inline ${
730 post_view.post.locked && 'text-danger'
733 <use xlinkHref="#icon-lock"></use>
737 class="btn btn-link btn-animate text-muted py-0"
738 onClick={linkEvent(this, this.handleModSticky)}
740 post_view.post.stickied
746 class={`icon icon-inline ${
747 post_view.post.stickied && 'text-success'
750 <use xlinkHref="#icon-pin"></use>
755 {/* Mods can ban from community, and appoint as mods to community */}
756 {(this.canMod || this.canAdmin) &&
757 (!post_view.post.removed ? (
759 class="btn btn-link btn-animate text-muted py-0"
760 onClick={linkEvent(this, this.handleModRemoveShow)}
766 class="btn btn-link btn-animate text-muted py-0"
767 onClick={linkEvent(this, this.handleModRemoveSubmit)}
775 (!post_view.creator_banned_from_community ? (
777 class="btn btn-link btn-animate text-muted py-0"
780 this.handleModBanFromCommunityShow
787 class="btn btn-link btn-animate text-muted py-0"
790 this.handleModBanFromCommunitySubmit
796 {!post_view.creator_banned_from_community &&
797 post_view.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_view.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 */}
849 (!post_view.creator.banned ? (
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_view.creator.banned && post_view.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_view;
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 let post = this.props.post_view.post;
986 return post.thumbnail_url || isImage(post.url) ? (
988 <div className={`${this.state.imageExpanded ? 'col-12' : 'col-8'}`}>
989 {this.postTitleLine()}
992 {/* Post body prev or thumbnail */}
993 {!this.state.imageExpanded && this.thumbnail()}
1001 showMobilePreview() {
1002 let post = this.props.post_view.post;
1005 !this.props.showBody && (
1007 className="md-div mb-1"
1008 dangerouslySetInnerHTML={{
1009 __html: md.render(previewLines(post.body)),
1019 {/* The mobile view*/}
1020 <div class="d-block d-sm-none">
1022 <div class="col-12">
1023 {this.createdLine()}
1025 {/* If it has a thumbnail, do a right aligned thumbnail */}
1026 {this.mobileThumbnail()}
1028 {/* Show a preview of the post body */}
1029 {this.showMobilePreview()}
1031 {this.commentsLine(true)}
1032 {this.duplicatesLine()}
1033 {this.removeAndBanDialogs()}
1038 {/* The larger view*/}
1039 <div class="d-none d-sm-block">
1042 {!this.state.imageExpanded && (
1043 <div class="col-sm-2 pr-0">
1044 <div class="">{this.thumbnail()}</div>
1049 this.state.imageExpanded ? 'col-12' : 'col-12 col-sm-9'
1053 <div className="col-12">
1054 {this.postTitleLine()}
1055 {this.createdLine()}
1056 {this.commentsLine()}
1057 {this.duplicatesLine()}
1058 {this.postActions()}
1059 {this.removeAndBanDialogs()}
1069 private get myPost(): boolean {
1071 UserService.Instance.user &&
1072 this.props.post_view.creator.id == UserService.Instance.user.id
1076 get isMod(): boolean {
1078 this.props.moderators &&
1080 this.props.moderators.map(m => m.moderator.id),
1081 this.props.post_view.creator.id
1086 get isAdmin(): boolean {
1088 this.props.admins &&
1090 this.props.admins.map(a => a.user.id),
1091 this.props.post_view.creator.id
1096 get canMod(): boolean {
1097 if (this.props.admins && this.props.moderators) {
1098 let adminsThenMods = this.props.admins
1099 .map(a => a.user.id)
1100 .concat(this.props.moderators.map(m => m.moderator.id));
1103 UserService.Instance.user,
1105 this.props.post_view.creator.id
1112 get canModOnSelf(): boolean {
1113 if (this.props.admins && this.props.moderators) {
1114 let adminsThenMods = this.props.admins
1115 .map(a => a.user.id)
1116 .concat(this.props.moderators.map(m => m.moderator.id));
1119 UserService.Instance.user,
1121 this.props.post_view.creator.id,
1129 get canAdmin(): boolean {
1131 this.props.admins &&
1133 UserService.Instance.user,
1134 this.props.admins.map(a => a.user.id),
1135 this.props.post_view.creator.id
1140 get amCommunityCreator(): boolean {
1142 this.props.moderators &&
1143 UserService.Instance.user &&
1144 this.props.post_view.creator.id != UserService.Instance.user.id &&
1145 UserService.Instance.user.id == this.props.moderators[0].moderator.id
1149 get amSiteCreator(): boolean {
1151 this.props.admins &&
1152 UserService.Instance.user &&
1153 this.props.post_view.creator.id != UserService.Instance.user.id &&
1154 UserService.Instance.user.id == this.props.admins[0].user.id
1158 handlePostLike(i: PostListing) {
1159 if (!UserService.Instance.user) {
1160 this.context.router.history.push(`/login`);
1163 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1165 if (i.state.my_vote == 1) {
1168 } else if (i.state.my_vote == -1) {
1169 i.state.downvotes--;
1177 i.state.my_vote = new_vote;
1179 let form: CreatePostLike = {
1180 post_id: i.props.post_view.post.id,
1181 score: i.state.my_vote,
1185 WebSocketService.Instance.send(wsClient.likePost(form));
1186 i.setState(i.state);
1190 handlePostDisLike(i: PostListing) {
1191 if (!UserService.Instance.user) {
1192 this.context.router.history.push(`/login`);
1195 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1197 if (i.state.my_vote == 1) {
1200 i.state.downvotes++;
1201 } else if (i.state.my_vote == -1) {
1202 i.state.downvotes--;
1205 i.state.downvotes++;
1209 i.state.my_vote = new_vote;
1211 let form: CreatePostLike = {
1212 post_id: i.props.post_view.post.id,
1213 score: i.state.my_vote,
1217 WebSocketService.Instance.send(wsClient.likePost(form));
1218 i.setState(i.state);
1222 handleEditClick(i: PostListing) {
1223 i.state.showEdit = true;
1224 i.setState(i.state);
1227 handleEditCancel() {
1228 this.state.showEdit = false;
1229 this.setState(this.state);
1232 // The actual editing is done in the recieve for post
1234 this.state.showEdit = false;
1235 this.setState(this.state);
1238 handleDeleteClick(i: PostListing) {
1239 let deleteForm: DeletePost = {
1240 edit_id: i.props.post_view.post.id,
1241 deleted: !i.props.post_view.post.deleted,
1244 WebSocketService.Instance.send(wsClient.deletePost(deleteForm));
1247 handleSavePostClick(i: PostListing) {
1249 i.props.post_view.saved == undefined ? true : !i.props.post_view.saved;
1250 let form: SavePost = {
1251 post_id: i.props.post_view.post.id,
1256 WebSocketService.Instance.send(wsClient.savePost(form));
1259 get crossPostParams(): string {
1260 let post = this.props.post_view.post;
1261 let params = `?title=${post.name}`;
1264 params += `&url=${encodeURIComponent(post.url)}`;
1267 params += `&body=${post.body}`;
1272 handleModRemoveShow(i: PostListing) {
1273 i.state.showRemoveDialog = true;
1274 i.setState(i.state);
1277 handleModRemoveReasonChange(i: PostListing, event: any) {
1278 i.state.removeReason = event.target.value;
1279 i.setState(i.state);
1282 handleModRemoveDataChange(i: PostListing, event: any) {
1283 i.state.removeData = event.target.checked;
1284 i.setState(i.state);
1287 handleModRemoveSubmit(i: PostListing) {
1288 event.preventDefault();
1289 let form: RemovePost = {
1290 edit_id: i.props.post_view.post.id,
1291 removed: !i.props.post_view.post.removed,
1292 reason: i.state.removeReason,
1295 WebSocketService.Instance.send(wsClient.removePost(form));
1297 i.state.showRemoveDialog = false;
1298 i.setState(i.state);
1301 handleModLock(i: PostListing) {
1302 let form: LockPost = {
1303 edit_id: i.props.post_view.post.id,
1304 locked: !i.props.post_view.post.locked,
1307 WebSocketService.Instance.send(wsClient.lockPost(form));
1310 handleModSticky(i: PostListing) {
1311 let form: StickyPost = {
1312 edit_id: i.props.post_view.post.id,
1313 stickied: !i.props.post_view.post.stickied,
1316 WebSocketService.Instance.send(wsClient.stickyPost(form));
1319 handleModBanFromCommunityShow(i: PostListing) {
1320 i.state.showBanDialog = true;
1321 i.state.banType = BanType.Community;
1322 i.setState(i.state);
1325 handleModBanShow(i: PostListing) {
1326 i.state.showBanDialog = true;
1327 i.state.banType = BanType.Site;
1328 i.setState(i.state);
1331 handleModBanReasonChange(i: PostListing, event: any) {
1332 i.state.banReason = event.target.value;
1333 i.setState(i.state);
1336 handleModBanExpiresChange(i: PostListing, event: any) {
1337 i.state.banExpires = event.target.value;
1338 i.setState(i.state);
1341 handleModBanFromCommunitySubmit(i: PostListing) {
1342 i.state.banType = BanType.Community;
1343 i.setState(i.state);
1344 i.handleModBanBothSubmit(i);
1347 handleModBanSubmit(i: PostListing) {
1348 i.state.banType = BanType.Site;
1349 i.setState(i.state);
1350 i.handleModBanBothSubmit(i);
1353 handleModBanBothSubmit(i: PostListing) {
1354 event.preventDefault();
1356 if (i.state.banType == BanType.Community) {
1357 // If its an unban, restore all their data
1358 let ban = !i.props.post_view.creator_banned_from_community;
1360 i.state.removeData = false;
1362 let form: BanFromCommunity = {
1363 user_id: i.props.post_view.creator.id,
1364 community_id: i.props.post_view.community.id,
1366 remove_data: i.state.removeData,
1367 reason: i.state.banReason,
1368 expires: getUnixTime(i.state.banExpires),
1371 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1373 // If its an unban, restore all their data
1374 let ban = !i.props.post_view.creator.banned;
1376 i.state.removeData = false;
1378 let form: BanUser = {
1379 user_id: i.props.post_view.creator.id,
1381 remove_data: i.state.removeData,
1382 reason: i.state.banReason,
1383 expires: getUnixTime(i.state.banExpires),
1386 WebSocketService.Instance.send(wsClient.banUser(form));
1389 i.state.showBanDialog = false;
1390 i.setState(i.state);
1393 handleAddModToCommunity(i: PostListing) {
1394 let form: AddModToCommunity = {
1395 user_id: i.props.post_view.creator.id,
1396 community_id: i.props.post_view.community.id,
1400 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1401 i.setState(i.state);
1404 handleAddAdmin(i: PostListing) {
1405 let form: AddAdmin = {
1406 user_id: i.props.post_view.creator.id,
1410 WebSocketService.Instance.send(wsClient.addAdmin(form));
1411 i.setState(i.state);
1414 handleShowConfirmTransferCommunity(i: PostListing) {
1415 i.state.showConfirmTransferCommunity = true;
1416 i.setState(i.state);
1419 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1420 i.state.showConfirmTransferCommunity = false;
1421 i.setState(i.state);
1424 handleTransferCommunity(i: PostListing) {
1425 let form: TransferCommunity = {
1426 community_id: i.props.post_view.community.id,
1427 user_id: i.props.post_view.creator.id,
1430 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1431 i.state.showConfirmTransferCommunity = false;
1432 i.setState(i.state);
1435 handleShowConfirmTransferSite(i: PostListing) {
1436 i.state.showConfirmTransferSite = true;
1437 i.setState(i.state);
1440 handleCancelShowConfirmTransferSite(i: PostListing) {
1441 i.state.showConfirmTransferSite = false;
1442 i.setState(i.state);
1445 handleTransferSite(i: PostListing) {
1446 let form: TransferSite = {
1447 user_id: i.props.post_view.creator.id,
1450 WebSocketService.Instance.send(wsClient.transferSite(form));
1451 i.state.showConfirmTransferSite = false;
1452 i.setState(i.state);
1455 handleImageExpandClick(i: PostListing) {
1456 i.state.imageExpanded = !i.state.imageExpanded;
1457 i.setState(i.state);
1460 handleViewSource(i: PostListing) {
1461 i.state.viewSource = !i.state.viewSource;
1462 i.setState(i.state);
1465 handleShowAdvanced(i: PostListing) {
1466 i.state.showAdvanced = !i.state.showAdvanced;
1467 i.setState(i.state);
1471 handleShowMoreMobile(i: PostListing) {
1472 i.state.showMoreMobile = !i.state.showMoreMobile;
1473 i.state.showAdvanced = !i.state.showAdvanced;
1474 i.setState(i.state);
1478 get pointsTippy(): string {
1479 let points = i18n.t('number_of_points', {
1480 count: this.state.score,
1483 let upvotes = i18n.t('number_of_upvotes', {
1484 count: this.state.upvotes,
1487 let downvotes = i18n.t('number_of_downvotes', {
1488 count: this.state.downvotes,
1491 return `${points} • ${upvotes} • ${downvotes}`;