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="text-body"
328 className="text-body"
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-success`}>
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 avatar: post.creator_avatar,
445 local: post.creator_local,
446 actor_id: post.creator_actor_id,
447 published: post.creator_published,
452 <span className="mx-1 badge badge-light">
457 <span className="mx-1 badge badge-light">
461 {(post.banned_from_community || post.banned) && (
462 <span className="mx-1 badge badge-danger">
466 {this.props.showCommunity && (
468 <span> {i18n.t('to')} </span>
471 name: post.community_name,
472 id: post.community_id,
473 local: post.community_local,
474 actor_id: post.community_actor_id,
480 <li className="list-inline-item">•</li>
481 <li className="list-inline-item">
483 <MomentTime data={post} />
488 <li className="list-inline-item">•</li>
489 <li className="list-inline-item">
490 {/* Using a link with tippy doesn't work on touch devices unfortunately */}
492 className="text-muted"
493 data-tippy-content={md.render(previewLines(post.body))}
494 data-tippy-allowHtml={true}
495 to={`/post/${post.id}`}
497 <svg class="mr-1 icon icon-inline">
498 <use xlinkHref="#icon-book-open"></use>
505 <ul class="list-inline mb-1 text-muted small">
506 <li className="list-inline-item">
508 className="text-muted"
509 title={i18n.t('number_of_comments', {
510 count: post.number_of_comments,
512 to={`/post/${post.id}`}
514 <svg class="mr-1 icon icon-inline">
515 <use xlinkHref="#icon-message-square"></use>
517 {i18n.t('number_of_comments', {
518 count: post.number_of_comments,
522 {this.state.upvotes !== this.state.score && (
524 <li className="list-inline-item">•</li>
526 class="unselectable pointer mr-2"
527 data-tippy-content={this.pointsTippy}
529 <li className="list-inline-item">
530 <span className="text-muted">
531 <svg class="small icon icon-inline mr-1">
532 <use xlinkHref="#icon-arrow-up"></use>
537 <li className="list-inline-item">
538 <span className="text-muted">
539 <svg class="small icon icon-inline mr-1">
540 <use xlinkHref="#icon-arrow-down"></use>
542 {this.state.downvotes}
549 {this.props.post.duplicates && (
550 <ul class="list-inline mb-1 small text-muted">
552 <li className="list-inline-item mr-2">
553 {i18n.t('cross_posted_to')}
555 {this.props.post.duplicates.map(post => (
556 <li className="list-inline-item mr-2">
557 <Link to={`/post/${post.id}`}>
558 {post.community_name}
565 <ul class="list-inline mb-1 text-muted font-weight-bold">
566 {UserService.Instance.user && (
568 {this.props.showBody && (
570 <li className="list-inline-item">
572 class="btn btn-link btn-animate text-muted"
573 onClick={linkEvent(this, this.handleSavePostClick)}
575 post.saved ? i18n.t('unsave') : i18n.t('save')
579 class={`icon icon-inline ${
580 post.saved && 'text-warning'
583 <use xlinkHref="#icon-star"></use>
587 <li className="list-inline-item">
589 class="btn btn-link btn-animate text-muted"
590 to={`/create_post${this.crossPostParams}`}
591 title={i18n.t('cross_post')}
593 <svg class="icon icon-inline">
594 <use xlinkHref="#icon-copy"></use>
600 {this.myPost && this.props.showBody && (
602 <li className="list-inline-item">
604 class="btn btn-link btn-animate text-muted"
605 onClick={linkEvent(this, this.handleEditClick)}
606 data-tippy-content={i18n.t('edit')}
608 <svg class="icon icon-inline">
609 <use xlinkHref="#icon-edit"></use>
613 <li className="list-inline-item">
615 class="btn btn-link btn-animate text-muted"
616 onClick={linkEvent(this, this.handleDeleteClick)}
624 class={`icon icon-inline ${
625 post.deleted && 'text-danger'
628 <use xlinkHref="#icon-trash"></use>
635 {!this.state.showAdvanced && this.props.showBody ? (
636 <li className="list-inline-item">
638 class="btn btn-link btn-animate text-muted"
639 onClick={linkEvent(this, this.handleShowAdvanced)}
640 data-tippy-content={i18n.t('more')}
642 <svg class="icon icon-inline">
643 <use xlinkHref="#icon-more-vertical"></use>
649 {this.props.showBody && post.body && (
650 <li className="list-inline-item">
652 class="btn btn-link btn-animate text-muted"
653 onClick={linkEvent(this, this.handleViewSource)}
654 data-tippy-content={i18n.t('view_source')}
657 class={`icon icon-inline ${
658 this.state.viewSource && 'text-success'
661 <use xlinkHref="#icon-file-text"></use>
666 {this.canModOnSelf && (
668 <li className="list-inline-item">
670 class="btn btn-link btn-animate text-muted"
671 onClick={linkEvent(this, this.handleModLock)}
679 class={`icon icon-inline ${
680 post.locked && 'text-danger'
683 <use xlinkHref="#icon-lock"></use>
687 <li className="list-inline-item">
689 class="btn btn-link btn-animate text-muted"
690 onClick={linkEvent(this, this.handleModSticky)}
698 class={`icon icon-inline ${
699 post.stickied && 'text-success'
702 <use xlinkHref="#icon-pin"></use>
708 {/* Mods can ban from community, and appoint as mods to community */}
709 {(this.canMod || this.canAdmin) && (
710 <li className="list-inline-item">
716 this.handleModRemoveShow
726 this.handleModRemoveSubmit
737 <li className="list-inline-item">
738 {!post.banned_from_community ? (
743 this.handleModBanFromCommunityShow
753 this.handleModBanFromCommunitySubmit
761 {!post.banned_from_community && (
762 <li className="list-inline-item">
767 this.handleAddModToCommunity
771 ? i18n.t('remove_as_mod')
772 : i18n.t('appoint_as_mod')}
778 {/* Community creators and admins can transfer community to another mod */}
779 {(this.amCommunityCreator || this.canAdmin) &&
781 <li className="list-inline-item">
782 {!this.state.showConfirmTransferCommunity ? (
787 this.handleShowConfirmTransferCommunity
790 {i18n.t('transfer_community')}
794 <span class="d-inline-block mr-1">
795 {i18n.t('are_you_sure')}
798 class="pointer d-inline-block mr-1"
801 this.handleTransferCommunity
807 class="pointer d-inline-block"
811 .handleCancelShowConfirmTransferCommunity
820 {/* Admins can ban from all, and appoint other admins */}
824 <li className="list-inline-item">
830 this.handleModBanShow
833 {i18n.t('ban_from_site')}
840 this.handleModBanSubmit
843 {i18n.t('unban_from_site')}
849 <li className="list-inline-item">
852 onClick={linkEvent(this, this.handleAddAdmin)}
855 ? i18n.t('remove_as_admin')
856 : i18n.t('appoint_as_admin')}
862 {/* Site Creator can transfer to another admin */}
863 {this.amSiteCreator && this.isAdmin && (
864 <li className="list-inline-item">
865 {!this.state.showConfirmTransferSite ? (
870 this.handleShowConfirmTransferSite
873 {i18n.t('transfer_site')}
877 <span class="d-inline-block mr-1">
878 {i18n.t('are_you_sure')}
881 class="pointer d-inline-block mr-1"
884 this.handleTransferSite
890 class="pointer d-inline-block"
893 this.handleCancelShowConfirmTransferSite
907 {this.state.showRemoveDialog && (
910 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
914 class="form-control mr-2"
915 placeholder={i18n.t('reason')}
916 value={this.state.removeReason}
917 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
919 <button type="submit" class="btn btn-secondary">
920 {i18n.t('remove_post')}
924 {this.state.showBanDialog && (
925 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
926 <div class="form-group row">
927 <label class="col-form-label" htmlFor="post-listing-reason">
932 id="post-listing-reason"
933 class="form-control mr-2"
934 placeholder={i18n.t('reason')}
935 value={this.state.banReason}
936 onInput={linkEvent(this, this.handleModBanReasonChange)}
939 {/* TODO hold off on expires until later */}
940 {/* <div class="form-group row"> */}
941 {/* <label class="col-form-label">Expires</label> */}
942 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
944 <div class="form-group row">
945 <button type="submit" class="btn btn-secondary">
946 {i18n.t('ban')} {post.creator_name}
958 private get myPost(): boolean {
960 UserService.Instance.user &&
961 this.props.post.creator_id == UserService.Instance.user.id
965 get isMod(): boolean {
967 this.props.moderators &&
969 this.props.moderators.map(m => m.user_id),
970 this.props.post.creator_id
975 get isAdmin(): boolean {
979 this.props.admins.map(a => a.id),
980 this.props.post.creator_id
985 get canMod(): boolean {
986 if (this.props.admins && this.props.moderators) {
987 let adminsThenMods = this.props.admins
989 .concat(this.props.moderators.map(m => m.user_id));
992 UserService.Instance.user,
994 this.props.post.creator_id
1001 get canModOnSelf(): boolean {
1002 if (this.props.admins && this.props.moderators) {
1003 let adminsThenMods = this.props.admins
1005 .concat(this.props.moderators.map(m => m.user_id));
1008 UserService.Instance.user,
1010 this.props.post.creator_id,
1018 get canAdmin(): boolean {
1020 this.props.admins &&
1022 UserService.Instance.user,
1023 this.props.admins.map(a => a.id),
1024 this.props.post.creator_id
1029 get amCommunityCreator(): boolean {
1031 this.props.moderators &&
1032 UserService.Instance.user &&
1033 this.props.post.creator_id != UserService.Instance.user.id &&
1034 UserService.Instance.user.id == this.props.moderators[0].user_id
1038 get amSiteCreator(): boolean {
1040 this.props.admins &&
1041 UserService.Instance.user &&
1042 this.props.post.creator_id != UserService.Instance.user.id &&
1043 UserService.Instance.user.id == this.props.admins[0].id
1047 handlePostLike(i: PostListing) {
1048 if (!UserService.Instance.user) {
1049 this.context.router.history.push(`/login`);
1052 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1054 if (i.state.my_vote == 1) {
1057 } else if (i.state.my_vote == -1) {
1058 i.state.downvotes--;
1066 i.state.my_vote = new_vote;
1068 let form: CreatePostLikeForm = {
1069 post_id: i.props.post.id,
1070 score: i.state.my_vote,
1073 WebSocketService.Instance.likePost(form);
1074 i.setState(i.state);
1078 handlePostDisLike(i: PostListing) {
1079 if (!UserService.Instance.user) {
1080 this.context.router.history.push(`/login`);
1083 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1085 if (i.state.my_vote == 1) {
1088 i.state.downvotes++;
1089 } else if (i.state.my_vote == -1) {
1090 i.state.downvotes--;
1093 i.state.downvotes++;
1097 i.state.my_vote = new_vote;
1099 let form: CreatePostLikeForm = {
1100 post_id: i.props.post.id,
1101 score: i.state.my_vote,
1104 WebSocketService.Instance.likePost(form);
1105 i.setState(i.state);
1109 handleEditClick(i: PostListing) {
1110 i.state.showEdit = true;
1111 i.setState(i.state);
1114 handleEditCancel() {
1115 this.state.showEdit = false;
1116 this.setState(this.state);
1119 // The actual editing is done in the recieve for post
1121 this.state.showEdit = false;
1122 this.setState(this.state);
1125 handleDeleteClick(i: PostListing) {
1126 let deleteForm: DeletePostForm = {
1127 edit_id: i.props.post.id,
1128 deleted: !i.props.post.deleted,
1131 WebSocketService.Instance.deletePost(deleteForm);
1134 handleSavePostClick(i: PostListing) {
1135 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
1136 let form: SavePostForm = {
1137 post_id: i.props.post.id,
1141 WebSocketService.Instance.savePost(form);
1144 get crossPostParams(): string {
1145 let params = `?title=${this.props.post.name}`;
1146 let post = this.props.post;
1149 params += `&url=${post.url}`;
1151 if (this.props.post.body) {
1152 params += `&body=${this.props.post.body}`;
1157 handleModRemoveShow(i: PostListing) {
1158 i.state.showRemoveDialog = true;
1159 i.setState(i.state);
1162 handleModRemoveReasonChange(i: PostListing, event: any) {
1163 i.state.removeReason = event.target.value;
1164 i.setState(i.state);
1167 handleModRemoveSubmit(i: PostListing) {
1168 event.preventDefault();
1169 let form: RemovePostForm = {
1170 edit_id: i.props.post.id,
1171 removed: !i.props.post.removed,
1172 reason: i.state.removeReason,
1175 WebSocketService.Instance.removePost(form);
1177 i.state.showRemoveDialog = false;
1178 i.setState(i.state);
1181 handleModLock(i: PostListing) {
1182 let form: LockPostForm = {
1183 edit_id: i.props.post.id,
1184 locked: !i.props.post.locked,
1187 WebSocketService.Instance.lockPost(form);
1190 handleModSticky(i: PostListing) {
1191 let form: StickyPostForm = {
1192 edit_id: i.props.post.id,
1193 stickied: !i.props.post.stickied,
1196 WebSocketService.Instance.stickyPost(form);
1199 handleModBanFromCommunityShow(i: PostListing) {
1200 i.state.showBanDialog = true;
1201 i.state.banType = BanType.Community;
1202 i.setState(i.state);
1205 handleModBanShow(i: PostListing) {
1206 i.state.showBanDialog = true;
1207 i.state.banType = BanType.Site;
1208 i.setState(i.state);
1211 handleModBanReasonChange(i: PostListing, event: any) {
1212 i.state.banReason = event.target.value;
1213 i.setState(i.state);
1216 handleModBanExpiresChange(i: PostListing, event: any) {
1217 i.state.banExpires = event.target.value;
1218 i.setState(i.state);
1221 handleModBanFromCommunitySubmit(i: PostListing) {
1222 i.state.banType = BanType.Community;
1223 i.setState(i.state);
1224 i.handleModBanBothSubmit(i);
1227 handleModBanSubmit(i: PostListing) {
1228 i.state.banType = BanType.Site;
1229 i.setState(i.state);
1230 i.handleModBanBothSubmit(i);
1233 handleModBanBothSubmit(i: PostListing) {
1234 event.preventDefault();
1236 if (i.state.banType == BanType.Community) {
1237 let form: BanFromCommunityForm = {
1238 user_id: i.props.post.creator_id,
1239 community_id: i.props.post.community_id,
1240 ban: !i.props.post.banned_from_community,
1241 reason: i.state.banReason,
1242 expires: getUnixTime(i.state.banExpires),
1244 WebSocketService.Instance.banFromCommunity(form);
1246 let form: BanUserForm = {
1247 user_id: i.props.post.creator_id,
1248 ban: !i.props.post.banned,
1249 reason: i.state.banReason,
1250 expires: getUnixTime(i.state.banExpires),
1252 WebSocketService.Instance.banUser(form);
1255 i.state.showBanDialog = false;
1256 i.setState(i.state);
1259 handleAddModToCommunity(i: PostListing) {
1260 let form: AddModToCommunityForm = {
1261 user_id: i.props.post.creator_id,
1262 community_id: i.props.post.community_id,
1265 WebSocketService.Instance.addModToCommunity(form);
1266 i.setState(i.state);
1269 handleAddAdmin(i: PostListing) {
1270 let form: AddAdminForm = {
1271 user_id: i.props.post.creator_id,
1274 WebSocketService.Instance.addAdmin(form);
1275 i.setState(i.state);
1278 handleShowConfirmTransferCommunity(i: PostListing) {
1279 i.state.showConfirmTransferCommunity = true;
1280 i.setState(i.state);
1283 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1284 i.state.showConfirmTransferCommunity = false;
1285 i.setState(i.state);
1288 handleTransferCommunity(i: PostListing) {
1289 let form: TransferCommunityForm = {
1290 community_id: i.props.post.community_id,
1291 user_id: i.props.post.creator_id,
1293 WebSocketService.Instance.transferCommunity(form);
1294 i.state.showConfirmTransferCommunity = false;
1295 i.setState(i.state);
1298 handleShowConfirmTransferSite(i: PostListing) {
1299 i.state.showConfirmTransferSite = true;
1300 i.setState(i.state);
1303 handleCancelShowConfirmTransferSite(i: PostListing) {
1304 i.state.showConfirmTransferSite = false;
1305 i.setState(i.state);
1308 handleTransferSite(i: PostListing) {
1309 let form: TransferSiteForm = {
1310 user_id: i.props.post.creator_id,
1312 WebSocketService.Instance.transferSite(form);
1313 i.state.showConfirmTransferSite = false;
1314 i.setState(i.state);
1317 handleImageExpandClick(i: PostListing) {
1318 i.state.imageExpanded = !i.state.imageExpanded;
1319 i.setState(i.state);
1322 handleViewSource(i: PostListing) {
1323 i.state.viewSource = !i.state.viewSource;
1324 i.setState(i.state);
1327 handleShowAdvanced(i: PostListing) {
1328 i.state.showAdvanced = !i.state.showAdvanced;
1329 i.setState(i.state);
1333 get pointsTippy(): string {
1334 let points = i18n.t('number_of_points', {
1335 count: this.state.score,
1338 let upvotes = i18n.t('number_of_upvotes', {
1339 count: this.state.upvotes,
1342 let downvotes = i18n.t('number_of_downvotes', {
1343 count: this.state.downvotes,
1346 return `${points} • ${upvotes} • ${downvotes}`;