1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { WebSocketService, UserService } from '../services';
17 AddModToCommunityForm,
20 TransferCommunityForm,
21 } 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';
42 interface PostListingState {
44 showRemoveDialog: boolean;
46 showBanDialog: boolean;
50 showConfirmTransferSite: boolean;
51 showConfirmTransferCommunity: boolean;
52 imageExpanded: boolean;
54 showAdvanced: boolean;
61 interface PostListingProps {
63 showCommunity?: boolean;
65 moderators?: Array<CommunityUser>;
66 admins?: Array<UserView>;
67 enableDownvotes: boolean;
71 export class PostListing extends Component<PostListingProps, PostListingState> {
72 private emptyState: PostListingState = {
74 showRemoveDialog: false,
79 banType: BanType.Community,
80 showConfirmTransferSite: false,
81 showConfirmTransferCommunity: false,
85 my_vote: this.props.post.my_vote,
86 score: this.props.post.score,
87 upvotes: this.props.post.upvotes,
88 downvotes: this.props.post.downvotes,
91 constructor(props: any, context: any) {
92 super(props, context);
94 this.state = this.emptyState;
95 this.handlePostLike = this.handlePostLike.bind(this);
96 this.handlePostDisLike = this.handlePostDisLike.bind(this);
97 this.handleEditPost = this.handleEditPost.bind(this);
98 this.handleEditCancel = this.handleEditCancel.bind(this);
101 componentWillReceiveProps(nextProps: PostListingProps) {
102 this.state.my_vote = nextProps.post.my_vote;
103 this.state.upvotes = nextProps.post.upvotes;
104 this.state.downvotes = nextProps.post.downvotes;
105 this.state.score = nextProps.post.score;
106 if (this.props.post.id !== nextProps.post.id) {
107 this.state.imageExpanded = false;
109 this.setState(this.state);
115 {!this.state.showEdit ? (
123 post={this.props.post}
124 onEdit={this.handleEditPost}
125 onCancel={this.handleEditCancel}
126 enableNsfw={this.props.enableNsfw}
127 enableDownvotes={this.props.enableDownvotes}
139 {this.props.post.url &&
140 this.props.showBody &&
141 this.props.post.embed_title && (
142 <IFramelyCard post={this.props.post} />
144 {this.props.showBody && this.props.post.body && (
146 {this.state.viewSource ? (
147 <pre>{this.props.post.body}</pre>
151 dangerouslySetInnerHTML={mdToHtml(this.props.post.body)}
161 imgThumb(src: string) {
162 let post = this.props.post;
165 className={`img-fluid thumbnail rounded ${
166 (post.nsfw || post.community_nsfw) && 'img-blur'
173 getImage(thumbnail: boolean = false) {
174 let post = this.props.post;
175 if (isImage(post.url)) {
176 if (post.url.includes('pictrs')) {
177 return pictrsImage(post.url, thumbnail);
178 } else if (post.thumbnail_url) {
179 return pictrsImage(post.thumbnail_url, thumbnail);
183 } else if (post.thumbnail_url) {
184 return pictrsImage(post.thumbnail_url, thumbnail);
189 let post = this.props.post;
191 if (isImage(post.url)) {
194 class="text-body pointer"
195 data-tippy-content={i18n.t('expand_here')}
196 onClick={linkEvent(this, this.handleImageExpandClick)}
198 {this.imgThumb(this.getImage(true))}
199 <svg class="icon mini-overlay">
200 <use xlinkHref="#icon-image"></use>
204 } else if (post.thumbnail_url) {
207 className="text-body"
213 {this.imgThumb(this.getImage(true))}
214 <svg class="icon mini-overlay">
215 <use xlinkHref="#icon-external-link"></use>
219 } else if (post.url) {
220 if (isVideo(post.url)) {
222 <div class="embed-responsive embed-responsive-16by9">
228 class="embed-responsive-item"
230 <source src={post.url} type="video/mp4" />
237 className="text-body"
243 <div class="thumbnail rounded bg-light d-flex justify-content-center">
244 <svg class="icon d-flex align-items-center">
245 <use xlinkHref="#icon-external-link"></use>
254 className="text-body"
255 to={`/post/${post.id}`}
256 title={i18n.t('comments')}
258 <div class="thumbnail rounded bg-light d-flex justify-content-center">
259 <svg class="icon d-flex align-items-center">
260 <use xlinkHref="#icon-message-square"></use>
269 let post = this.props.post;
272 <div className={`vote-bar col-1 pr-0 small text-center`}>
274 className={`btn-animate btn btn-link p-0 ${
275 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
277 onClick={linkEvent(this, this.handlePostLike)}
278 data-tippy-content={i18n.t('upvote')}
280 <svg class="icon upvote">
281 <use xlinkHref="#icon-arrow-up1"></use>
285 class={`unselectable pointer font-weight-bold text-muted px-1`}
286 data-tippy-content={this.pointsTippy}
290 {this.props.enableDownvotes && (
292 className={`btn-animate btn btn-link p-0 ${
293 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
295 onClick={linkEvent(this, this.handlePostDisLike)}
296 data-tippy-content={i18n.t('downvote')}
298 <svg class="icon downvote">
299 <use xlinkHref="#icon-arrow-down1"></use>
304 {!this.state.imageExpanded && (
305 <div class="col-3 col-sm-2 pr-0">
306 <div class="position-relative">{this.thumbnail()}</div>
310 class={`${this.state.imageExpanded ? 'col-12' : 'col-8 col-sm-9'}`}
313 <div className="col-12">
314 <div className="post-title">
315 <h5 className="mb-1 d-inline-block">
316 {this.props.showBody && post.url ? (
318 className={!post.stickied ? 'text-body' : 'text-primary'}
328 className={!post.stickied ? 'text-body' : 'text-primary'}
329 to={`/post/${post.id}`}
330 title={i18n.t('comments')}
336 {post.url && !(hostname(post.url) == window.location.hostname) && (
337 <small class="d-inline-block">
339 className="ml-2 text-muted font-italic"
346 <svg class="ml-1 icon icon-inline">
347 <use xlinkHref="#icon-external-link"></use>
352 {(isImage(post.url) || this.props.post.thumbnail_url) && (
354 {!this.state.imageExpanded ? (
356 class="text-monospace unselectable pointer ml-2 text-muted small"
357 data-tippy-content={i18n.t('expand_here')}
358 onClick={linkEvent(this, this.handleImageExpandClick)}
360 <svg class="icon icon-inline">
361 <use xlinkHref="#icon-plus-square"></use>
367 class="text-monospace unselectable pointer ml-2 text-muted small"
368 onClick={linkEvent(this, this.handleImageExpandClick)}
370 <svg class="icon icon-inline">
371 <use xlinkHref="#icon-minus-square"></use>
379 this.handleImageExpandClick
383 class="img-fluid img-expanded"
384 src={this.getImage()}
393 <small className="ml-2 text-muted font-italic">
399 className="unselectable pointer ml-2 text-muted font-italic"
400 data-tippy-content={i18n.t('deleted')}
402 <svg class={`icon icon-inline text-danger`}>
403 <use xlinkHref="#icon-trash"></use>
409 className="unselectable pointer ml-2 text-muted font-italic"
410 data-tippy-content={i18n.t('locked')}
412 <svg class={`icon icon-inline text-danger`}>
413 <use xlinkHref="#icon-lock"></use>
419 className="unselectable pointer ml-2 text-muted font-italic"
420 data-tippy-content={i18n.t('stickied')}
422 <svg class={`icon icon-inline text-primary`}>
423 <use xlinkHref="#icon-pin"></use>
428 <small className="ml-2 text-muted font-italic">
436 <div className="details col-12">
437 <ul class="list-inline mb-1 text-muted small">
438 <li className="list-inline-item">
439 <span>{i18n.t('by')} </span>
442 name: post.creator_name,
443 preferred_username: post.creator_preferred_username,
444 avatar: post.creator_avatar,
446 local: post.creator_local,
447 actor_id: post.creator_actor_id,
448 published: post.creator_published,
453 <span className="mx-1 badge badge-light">
458 <span className="mx-1 badge badge-light">
462 {(post.banned_from_community || post.banned) && (
463 <span className="mx-1 badge badge-danger">
467 {this.props.showCommunity && (
469 <span> {i18n.t('to')} </span>
472 name: post.community_name,
473 id: post.community_id,
474 local: post.community_local,
475 actor_id: post.community_actor_id,
476 icon: post.community_icon,
482 <li className="list-inline-item">•</li>
483 <li className="list-inline-item">
485 <MomentTime data={post} />
490 <li className="list-inline-item">•</li>
491 <li className="list-inline-item">
492 {/* Using a link with tippy doesn't work on touch devices unfortunately */}
494 className="text-muted"
495 data-tippy-content={md.render(previewLines(post.body))}
496 data-tippy-allowHtml={true}
497 to={`/post/${post.id}`}
499 <svg class="mr-1 icon icon-inline">
500 <use xlinkHref="#icon-book-open"></use>
507 <ul class="list-inline mb-1 text-muted small">
508 <li className="list-inline-item">
510 className="text-muted"
511 title={i18n.t('number_of_comments', {
512 count: post.number_of_comments,
514 to={`/post/${post.id}`}
516 <svg class="mr-1 icon icon-inline">
517 <use xlinkHref="#icon-message-square"></use>
519 {i18n.t('number_of_comments', {
520 count: post.number_of_comments,
524 {this.state.upvotes !== this.state.score && (
526 <li className="list-inline-item">•</li>
528 class="unselectable pointer mr-2"
529 data-tippy-content={this.pointsTippy}
531 <li className="list-inline-item">
532 <span className="text-muted">
533 <svg class="small icon icon-inline mr-1">
534 <use xlinkHref="#icon-arrow-up"></use>
539 <li className="list-inline-item">
540 <span className="text-muted">
541 <svg class="small icon icon-inline mr-1">
542 <use xlinkHref="#icon-arrow-down"></use>
544 {this.state.downvotes}
551 {this.props.post.duplicates && (
552 <ul class="list-inline mb-1 small text-muted">
554 <li className="list-inline-item mr-2">
555 {i18n.t('cross_posted_to')}
557 {this.props.post.duplicates.map(post => (
558 <li className="list-inline-item mr-2">
559 <Link to={`/post/${post.id}`}>
560 {post.community_name}
567 <ul class="list-inline mb-1 text-muted font-weight-bold">
568 {UserService.Instance.user && (
570 {this.props.showBody && (
572 <li className="list-inline-item">
574 class="btn btn-link btn-animate text-muted"
575 onClick={linkEvent(this, this.handleSavePostClick)}
577 post.saved ? i18n.t('unsave') : i18n.t('save')
581 class={`icon icon-inline ${
582 post.saved && 'text-warning'
585 <use xlinkHref="#icon-star"></use>
589 <li className="list-inline-item">
591 class="btn btn-link btn-animate text-muted"
592 to={`/create_post${this.crossPostParams}`}
593 title={i18n.t('cross_post')}
595 <svg class="icon icon-inline">
596 <use xlinkHref="#icon-copy"></use>
602 {this.myPost && this.props.showBody && (
604 <li className="list-inline-item">
606 class="btn btn-link btn-animate text-muted"
607 onClick={linkEvent(this, this.handleEditClick)}
608 data-tippy-content={i18n.t('edit')}
610 <svg class="icon icon-inline">
611 <use xlinkHref="#icon-edit"></use>
615 <li className="list-inline-item">
617 class="btn btn-link btn-animate text-muted"
618 onClick={linkEvent(this, this.handleDeleteClick)}
626 class={`icon icon-inline ${
627 post.deleted && 'text-danger'
630 <use xlinkHref="#icon-trash"></use>
637 {!this.state.showAdvanced && this.props.showBody ? (
638 <li className="list-inline-item">
640 class="btn btn-link btn-animate text-muted"
641 onClick={linkEvent(this, this.handleShowAdvanced)}
642 data-tippy-content={i18n.t('more')}
644 <svg class="icon icon-inline">
645 <use xlinkHref="#icon-more-vertical"></use>
651 {this.props.showBody && post.body && (
652 <li className="list-inline-item">
654 class="btn btn-link btn-animate text-muted"
655 onClick={linkEvent(this, this.handleViewSource)}
656 data-tippy-content={i18n.t('view_source')}
659 class={`icon icon-inline ${
660 this.state.viewSource && 'text-success'
663 <use xlinkHref="#icon-file-text"></use>
668 {this.canModOnSelf && (
670 <li className="list-inline-item">
672 class="btn btn-link btn-animate text-muted"
673 onClick={linkEvent(this, this.handleModLock)}
681 class={`icon icon-inline ${
682 post.locked && 'text-danger'
685 <use xlinkHref="#icon-lock"></use>
689 <li className="list-inline-item">
691 class="btn btn-link btn-animate text-muted"
692 onClick={linkEvent(this, this.handleModSticky)}
700 class={`icon icon-inline ${
701 post.stickied && 'text-success'
704 <use xlinkHref="#icon-pin"></use>
710 {/* Mods can ban from community, and appoint as mods to community */}
711 {(this.canMod || this.canAdmin) && (
712 <li className="list-inline-item">
718 this.handleModRemoveShow
728 this.handleModRemoveSubmit
739 <li className="list-inline-item">
740 {!post.banned_from_community ? (
745 this.handleModBanFromCommunityShow
755 this.handleModBanFromCommunitySubmit
763 {!post.banned_from_community && (
764 <li className="list-inline-item">
769 this.handleAddModToCommunity
773 ? i18n.t('remove_as_mod')
774 : i18n.t('appoint_as_mod')}
780 {/* Community creators and admins can transfer community to another mod */}
781 {(this.amCommunityCreator || this.canAdmin) &&
783 <li className="list-inline-item">
784 {!this.state.showConfirmTransferCommunity ? (
789 this.handleShowConfirmTransferCommunity
792 {i18n.t('transfer_community')}
796 <span class="d-inline-block mr-1">
797 {i18n.t('are_you_sure')}
800 class="pointer d-inline-block mr-1"
803 this.handleTransferCommunity
809 class="pointer d-inline-block"
813 .handleCancelShowConfirmTransferCommunity
822 {/* Admins can ban from all, and appoint other admins */}
826 <li className="list-inline-item">
832 this.handleModBanShow
835 {i18n.t('ban_from_site')}
842 this.handleModBanSubmit
845 {i18n.t('unban_from_site')}
851 <li className="list-inline-item">
854 onClick={linkEvent(this, this.handleAddAdmin)}
857 ? i18n.t('remove_as_admin')
858 : i18n.t('appoint_as_admin')}
864 {/* Site Creator can transfer to another admin */}
865 {this.amSiteCreator && this.isAdmin && (
866 <li className="list-inline-item">
867 {!this.state.showConfirmTransferSite ? (
872 this.handleShowConfirmTransferSite
875 {i18n.t('transfer_site')}
879 <span class="d-inline-block mr-1">
880 {i18n.t('are_you_sure')}
883 class="pointer d-inline-block mr-1"
886 this.handleTransferSite
892 class="pointer d-inline-block"
895 this.handleCancelShowConfirmTransferSite
909 {this.state.showRemoveDialog && (
912 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
916 class="form-control mr-2"
917 placeholder={i18n.t('reason')}
918 value={this.state.removeReason}
919 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
921 <button type="submit" class="btn btn-secondary">
922 {i18n.t('remove_post')}
926 {this.state.showBanDialog && (
927 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
928 <div class="form-group row">
929 <label class="col-form-label" htmlFor="post-listing-reason">
934 id="post-listing-reason"
935 class="form-control mr-2"
936 placeholder={i18n.t('reason')}
937 value={this.state.banReason}
938 onInput={linkEvent(this, this.handleModBanReasonChange)}
941 {/* TODO hold off on expires until later */}
942 {/* <div class="form-group row"> */}
943 {/* <label class="col-form-label">Expires</label> */}
944 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
946 <div class="form-group row">
947 <button type="submit" class="btn btn-secondary">
948 {i18n.t('ban')} {post.creator_name}
960 private get myPost(): boolean {
962 UserService.Instance.user &&
963 this.props.post.creator_id == UserService.Instance.user.id
967 get isMod(): boolean {
969 this.props.moderators &&
971 this.props.moderators.map(m => m.user_id),
972 this.props.post.creator_id
977 get isAdmin(): boolean {
981 this.props.admins.map(a => a.id),
982 this.props.post.creator_id
987 get canMod(): boolean {
988 if (this.props.admins && this.props.moderators) {
989 let adminsThenMods = this.props.admins
991 .concat(this.props.moderators.map(m => m.user_id));
994 UserService.Instance.user,
996 this.props.post.creator_id
1003 get canModOnSelf(): boolean {
1004 if (this.props.admins && this.props.moderators) {
1005 let adminsThenMods = this.props.admins
1007 .concat(this.props.moderators.map(m => m.user_id));
1010 UserService.Instance.user,
1012 this.props.post.creator_id,
1020 get canAdmin(): boolean {
1022 this.props.admins &&
1024 UserService.Instance.user,
1025 this.props.admins.map(a => a.id),
1026 this.props.post.creator_id
1031 get amCommunityCreator(): boolean {
1033 this.props.moderators &&
1034 UserService.Instance.user &&
1035 this.props.post.creator_id != UserService.Instance.user.id &&
1036 UserService.Instance.user.id == this.props.moderators[0].user_id
1040 get amSiteCreator(): boolean {
1042 this.props.admins &&
1043 UserService.Instance.user &&
1044 this.props.post.creator_id != UserService.Instance.user.id &&
1045 UserService.Instance.user.id == this.props.admins[0].id
1049 handlePostLike(i: PostListing) {
1050 if (!UserService.Instance.user) {
1051 this.context.router.history.push(`/login`);
1054 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1056 if (i.state.my_vote == 1) {
1059 } else if (i.state.my_vote == -1) {
1060 i.state.downvotes--;
1068 i.state.my_vote = new_vote;
1070 let form: CreatePostLikeForm = {
1071 post_id: i.props.post.id,
1072 score: i.state.my_vote,
1075 WebSocketService.Instance.likePost(form);
1076 i.setState(i.state);
1080 handlePostDisLike(i: PostListing) {
1081 if (!UserService.Instance.user) {
1082 this.context.router.history.push(`/login`);
1085 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1087 if (i.state.my_vote == 1) {
1090 i.state.downvotes++;
1091 } else if (i.state.my_vote == -1) {
1092 i.state.downvotes--;
1095 i.state.downvotes++;
1099 i.state.my_vote = new_vote;
1101 let form: CreatePostLikeForm = {
1102 post_id: i.props.post.id,
1103 score: i.state.my_vote,
1106 WebSocketService.Instance.likePost(form);
1107 i.setState(i.state);
1111 handleEditClick(i: PostListing) {
1112 i.state.showEdit = true;
1113 i.setState(i.state);
1116 handleEditCancel() {
1117 this.state.showEdit = false;
1118 this.setState(this.state);
1121 // The actual editing is done in the recieve for post
1123 this.state.showEdit = false;
1124 this.setState(this.state);
1127 handleDeleteClick(i: PostListing) {
1128 let deleteForm: DeletePostForm = {
1129 edit_id: i.props.post.id,
1130 deleted: !i.props.post.deleted,
1133 WebSocketService.Instance.deletePost(deleteForm);
1136 handleSavePostClick(i: PostListing) {
1137 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
1138 let form: SavePostForm = {
1139 post_id: i.props.post.id,
1143 WebSocketService.Instance.savePost(form);
1146 get crossPostParams(): string {
1147 let params = `?title=${this.props.post.name}`;
1148 let post = this.props.post;
1151 params += `&url=${post.url}`;
1153 if (this.props.post.body) {
1154 params += `&body=${this.props.post.body}`;
1159 handleModRemoveShow(i: PostListing) {
1160 i.state.showRemoveDialog = true;
1161 i.setState(i.state);
1164 handleModRemoveReasonChange(i: PostListing, event: any) {
1165 i.state.removeReason = event.target.value;
1166 i.setState(i.state);
1169 handleModRemoveSubmit(i: PostListing) {
1170 event.preventDefault();
1171 let form: RemovePostForm = {
1172 edit_id: i.props.post.id,
1173 removed: !i.props.post.removed,
1174 reason: i.state.removeReason,
1177 WebSocketService.Instance.removePost(form);
1179 i.state.showRemoveDialog = false;
1180 i.setState(i.state);
1183 handleModLock(i: PostListing) {
1184 let form: LockPostForm = {
1185 edit_id: i.props.post.id,
1186 locked: !i.props.post.locked,
1189 WebSocketService.Instance.lockPost(form);
1192 handleModSticky(i: PostListing) {
1193 let form: StickyPostForm = {
1194 edit_id: i.props.post.id,
1195 stickied: !i.props.post.stickied,
1198 WebSocketService.Instance.stickyPost(form);
1201 handleModBanFromCommunityShow(i: PostListing) {
1202 i.state.showBanDialog = true;
1203 i.state.banType = BanType.Community;
1204 i.setState(i.state);
1207 handleModBanShow(i: PostListing) {
1208 i.state.showBanDialog = true;
1209 i.state.banType = BanType.Site;
1210 i.setState(i.state);
1213 handleModBanReasonChange(i: PostListing, event: any) {
1214 i.state.banReason = event.target.value;
1215 i.setState(i.state);
1218 handleModBanExpiresChange(i: PostListing, event: any) {
1219 i.state.banExpires = event.target.value;
1220 i.setState(i.state);
1223 handleModBanFromCommunitySubmit(i: PostListing) {
1224 i.state.banType = BanType.Community;
1225 i.setState(i.state);
1226 i.handleModBanBothSubmit(i);
1229 handleModBanSubmit(i: PostListing) {
1230 i.state.banType = BanType.Site;
1231 i.setState(i.state);
1232 i.handleModBanBothSubmit(i);
1235 handleModBanBothSubmit(i: PostListing) {
1236 event.preventDefault();
1238 if (i.state.banType == BanType.Community) {
1239 let form: BanFromCommunityForm = {
1240 user_id: i.props.post.creator_id,
1241 community_id: i.props.post.community_id,
1242 ban: !i.props.post.banned_from_community,
1243 reason: i.state.banReason,
1244 expires: getUnixTime(i.state.banExpires),
1246 WebSocketService.Instance.banFromCommunity(form);
1248 let form: BanUserForm = {
1249 user_id: i.props.post.creator_id,
1250 ban: !i.props.post.banned,
1251 reason: i.state.banReason,
1252 expires: getUnixTime(i.state.banExpires),
1254 WebSocketService.Instance.banUser(form);
1257 i.state.showBanDialog = false;
1258 i.setState(i.state);
1261 handleAddModToCommunity(i: PostListing) {
1262 let form: AddModToCommunityForm = {
1263 user_id: i.props.post.creator_id,
1264 community_id: i.props.post.community_id,
1267 WebSocketService.Instance.addModToCommunity(form);
1268 i.setState(i.state);
1271 handleAddAdmin(i: PostListing) {
1272 let form: AddAdminForm = {
1273 user_id: i.props.post.creator_id,
1276 WebSocketService.Instance.addAdmin(form);
1277 i.setState(i.state);
1280 handleShowConfirmTransferCommunity(i: PostListing) {
1281 i.state.showConfirmTransferCommunity = true;
1282 i.setState(i.state);
1285 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1286 i.state.showConfirmTransferCommunity = false;
1287 i.setState(i.state);
1290 handleTransferCommunity(i: PostListing) {
1291 let form: TransferCommunityForm = {
1292 community_id: i.props.post.community_id,
1293 user_id: i.props.post.creator_id,
1295 WebSocketService.Instance.transferCommunity(form);
1296 i.state.showConfirmTransferCommunity = false;
1297 i.setState(i.state);
1300 handleShowConfirmTransferSite(i: PostListing) {
1301 i.state.showConfirmTransferSite = true;
1302 i.setState(i.state);
1305 handleCancelShowConfirmTransferSite(i: PostListing) {
1306 i.state.showConfirmTransferSite = false;
1307 i.setState(i.state);
1310 handleTransferSite(i: PostListing) {
1311 let form: TransferSiteForm = {
1312 user_id: i.props.post.creator_id,
1314 WebSocketService.Instance.transferSite(form);
1315 i.state.showConfirmTransferSite = false;
1316 i.setState(i.state);
1319 handleImageExpandClick(i: PostListing) {
1320 i.state.imageExpanded = !i.state.imageExpanded;
1321 i.setState(i.state);
1324 handleViewSource(i: PostListing) {
1325 i.state.viewSource = !i.state.viewSource;
1326 i.setState(i.state);
1329 handleShowAdvanced(i: PostListing) {
1330 i.state.showAdvanced = !i.state.showAdvanced;
1331 i.setState(i.state);
1335 get pointsTippy(): string {
1336 let points = i18n.t('number_of_points', {
1337 count: this.state.score,
1340 let upvotes = i18n.t('number_of_upvotes', {
1341 count: this.state.upvotes,
1344 let downvotes = i18n.t('number_of_downvotes', {
1345 count: this.state.downvotes,
1348 return `${points} • ${upvotes} • ${downvotes}`;