1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { WebSocketService, UserService } from '../services';
14 AddModToCommunityForm,
17 TransferCommunityForm,
18 } from '../interfaces';
19 import { MomentTime } from './moment-time';
20 import { PostForm } from './post-form';
21 import { IFramelyCard } from './iframely-card';
22 import { UserListing } from './user-listing';
36 import { i18n } from '../i18next';
38 interface PostListingState {
40 showRemoveDialog: boolean;
42 showBanDialog: boolean;
46 showConfirmTransferSite: boolean;
47 showConfirmTransferCommunity: boolean;
48 imageExpanded: boolean;
50 showAdvanced: boolean;
57 interface PostListingProps {
59 showCommunity?: boolean;
61 moderators?: Array<CommunityUser>;
62 admins?: Array<UserView>;
65 export class PostListing extends Component<PostListingProps, PostListingState> {
66 private emptyState: PostListingState = {
68 showRemoveDialog: false,
73 banType: BanType.Community,
74 showConfirmTransferSite: false,
75 showConfirmTransferCommunity: false,
79 my_vote: this.props.post.my_vote,
80 score: this.props.post.score,
81 upvotes: this.props.post.upvotes,
82 downvotes: this.props.post.downvotes,
85 constructor(props: any, context: any) {
86 super(props, context);
88 this.state = this.emptyState;
89 this.handlePostLike = this.handlePostLike.bind(this);
90 this.handlePostDisLike = this.handlePostDisLike.bind(this);
91 this.handleEditPost = this.handleEditPost.bind(this);
92 this.handleEditCancel = this.handleEditCancel.bind(this);
95 componentWillReceiveProps(nextProps: PostListingProps) {
96 this.state.my_vote = nextProps.post.my_vote;
97 this.state.upvotes = nextProps.post.upvotes;
98 this.state.downvotes = nextProps.post.downvotes;
99 this.state.score = nextProps.post.score;
100 this.setState(this.state);
106 {!this.state.showEdit ? (
114 post={this.props.post}
115 onEdit={this.handleEditPost}
116 onCancel={this.handleEditCancel}
128 {this.props.post.url &&
129 this.props.showBody &&
130 this.props.post.embed_title && (
131 <IFramelyCard post={this.props.post} />
133 {this.props.showBody && this.props.post.body && (
135 {this.state.viewSource ? (
136 <pre>{this.props.post.body}</pre>
140 dangerouslySetInnerHTML={mdToHtml(this.props.post.body)}
150 imgThumb(src: string) {
151 let post = this.props.post;
154 className={`img-fluid thumbnail rounded ${
155 (post.nsfw || post.community_nsfw) && 'img-blur'
162 getImage(thumbnail: boolean = false) {
163 let post = this.props.post;
164 if (isImage(post.url)) {
165 if (post.url.includes('pictshare')) {
166 return pictshareImage(post.url, thumbnail);
170 } else if (post.thumbnail_url) {
171 return pictshareImage(post.thumbnail_url, thumbnail);
176 let post = this.props.post;
178 if (isImage(post.url)) {
181 class="text-body pointer"
182 data-tippy-content={i18n.t('expand_here')}
183 onClick={linkEvent(this, this.handleImageExpandClick)}
185 {this.imgThumb(this.getImage(true))}
186 <svg class="icon mini-overlay">
187 <use xlinkHref="#icon-image"></use>
191 } else if (post.thumbnail_url) {
194 className="text-body"
199 {this.imgThumb(this.getImage(true))}
200 <svg class="icon mini-overlay">
201 <use xlinkHref="#icon-external-link"></use>
205 } else if (post.url) {
206 if (isVideo(post.url)) {
208 <div class="embed-responsive embed-responsive-16by9">
214 class="embed-responsive-item"
216 <source src={post.url} type="video/mp4" />
223 className="text-body"
228 <svg class="icon thumbnail">
229 <use xlinkHref="#icon-external-link"></use>
237 className="text-body"
238 to={`/post/${post.id}`}
239 title={i18n.t('comments')}
241 <svg class="icon thumbnail">
242 <use xlinkHref="#icon-message-square"></use>
250 let post = this.props.post;
253 <div className={`vote-bar col-1 pr-0 small text-center`}>
255 className={`btn-animate btn btn-link p-0 ${
256 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
258 onClick={linkEvent(this, this.handlePostLike)}
259 data-tippy-content={i18n.t('upvote')}
261 <svg class="icon upvote">
262 <use xlinkHref="#icon-arrow-up1"></use>
266 class={`unselectable pointer font-weight-bold text-muted px-1`}
267 data-tippy-content={this.pointsTippy}
271 {WebSocketService.Instance.site.enable_downvotes && (
273 className={`btn-animate btn btn-link p-0 ${
274 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
276 onClick={linkEvent(this, this.handlePostDisLike)}
277 data-tippy-content={i18n.t('downvote')}
279 <svg class="icon downvote">
280 <use xlinkHref="#icon-arrow-down1"></use>
285 {!this.state.imageExpanded && (
286 <div class="col-3 col-sm-2 pr-0 mt-1">
287 <div class="position-relative">{this.thumbnail()}</div>
291 class={`${this.state.imageExpanded ? 'col-12' : 'col-8 col-sm-9'}`}
294 <div className="col-12">
295 <div className="post-title">
296 <h5 className="mb-0 d-inline">
297 {this.props.showBody && post.url ? (
299 className="text-body"
308 className="text-body"
309 to={`/post/${post.id}`}
310 title={i18n.t('comments')}
316 {post.url && !(hostname(post.url) == window.location.hostname) && (
317 <small class="d-inline-block">
319 className="ml-2 text-muted font-italic"
325 <svg class="ml-1 icon icon-inline">
326 <use xlinkHref="#icon-external-link"></use>
331 {(isImage(post.url) || this.props.post.thumbnail_url) && (
333 {!this.state.imageExpanded ? (
335 class="text-monospace unselectable pointer ml-2 text-muted small"
336 data-tippy-content={i18n.t('expand_here')}
337 onClick={linkEvent(this, this.handleImageExpandClick)}
339 <svg class="icon icon-inline">
340 <use xlinkHref="#icon-plus-square"></use>
346 class="text-monospace unselectable pointer ml-2 text-muted small"
347 onClick={linkEvent(this, this.handleImageExpandClick)}
349 <svg class="icon icon-inline">
350 <use xlinkHref="#icon-minus-square"></use>
358 this.handleImageExpandClick
362 class="img-fluid img-expanded"
363 src={this.getImage()}
372 <small className="ml-2 text-muted font-italic">
378 className="unselectable pointer ml-2 text-muted font-italic"
379 data-tippy-content={i18n.t('deleted')}
381 <svg class={`icon icon-inline text-danger`}>
382 <use xlinkHref="#icon-trash"></use>
388 className="unselectable pointer ml-2 text-muted font-italic"
389 data-tippy-content={i18n.t('locked')}
391 <svg class={`icon icon-inline text-danger`}>
392 <use xlinkHref="#icon-lock"></use>
398 className="unselectable pointer ml-2 text-muted font-italic"
399 data-tippy-content={i18n.t('stickied')}
401 <svg class={`icon icon-inline text-success`}>
402 <use xlinkHref="#icon-pin"></use>
407 <small className="ml-2 text-muted font-italic">
415 <div className="details col-12">
416 <ul class="list-inline mb-0 text-muted small">
417 <li className="list-inline-item">
418 <span>{i18n.t('by')} </span>
421 name: post.creator_name,
422 avatar: post.creator_avatar,
426 <span className="mx-1 badge badge-light">
431 <span className="mx-1 badge badge-light">
435 {(post.banned_from_community || post.banned) && (
436 <span className="mx-1 badge badge-danger">
440 {this.props.showCommunity && (
442 <span> {i18n.t('to')} </span>
444 <Link to={`/c/${post.community_name}`}>
445 {post.community_name}
448 <a href={post.community_actor_id} target="_blank">
449 {hostname(post.ap_id)}/{post.community_name}
455 <li className="list-inline-item">•</li>
456 <li className="list-inline-item">
458 <MomentTime data={post} />
463 <li className="list-inline-item">•</li>
464 <li className="list-inline-item">
465 {/* Using a link with tippy doesn't work on touch devices unfortunately */}
467 className="text-muted"
468 data-tippy-content={md.render(previewLines(post.body))}
469 data-tippy-allowHtml={true}
470 to={`/post/${post.id}`}
472 <svg class="mr-1 icon icon-inline">
473 <use xlinkHref="#icon-book-open"></use>
479 <li className="list-inline-item">•</li>
480 {this.state.upvotes !== this.state.score && (
483 class="unselectable pointer mr-2"
484 data-tippy-content={this.pointsTippy}
486 <li className="list-inline-item">
487 <span className="text-muted">
488 <svg class="small icon icon-inline mr-1">
489 <use xlinkHref="#icon-arrow-up"></use>
494 <li className="list-inline-item">
495 <span className="text-muted">
496 <svg class="small icon icon-inline mr-1">
497 <use xlinkHref="#icon-arrow-down"></use>
499 {this.state.downvotes}
503 <li className="list-inline-item">•</li>
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 {post.number_of_comments}
521 {this.props.post.duplicates && (
522 <ul class="list-inline mb-1 small text-muted">
524 <li className="list-inline-item mr-2">
525 {i18n.t('cross_posted_to')}
527 {this.props.post.duplicates.map(post => (
528 <li className="list-inline-item mr-2">
529 <Link to={`/post/${post.id}`}>
530 {post.community_name}
537 <ul class="list-inline mb-1 text-muted font-weight-bold">
538 {UserService.Instance.user && (
540 {this.props.showBody && (
542 <li className="list-inline-item">
544 class="btn btn-sm btn-link btn-animate text-muted"
545 onClick={linkEvent(this, this.handleSavePostClick)}
547 post.saved ? i18n.t('unsave') : i18n.t('save')
551 class={`icon icon-inline ${
552 post.saved && 'text-warning'
555 <use xlinkHref="#icon-star"></use>
559 <li className="list-inline-item">
561 class="btn btn-sm btn-link btn-animate text-muted"
562 to={`/create_post${this.crossPostParams}`}
563 title={i18n.t('cross_post')}
565 <svg class="icon icon-inline">
566 <use xlinkHref="#icon-copy"></use>
572 {this.myPost && this.props.showBody && (
574 <li className="list-inline-item">
576 class="btn btn-sm btn-link btn-animate text-muted"
577 onClick={linkEvent(this, this.handleEditClick)}
578 data-tippy-content={i18n.t('edit')}
580 <svg class="icon icon-inline">
581 <use xlinkHref="#icon-edit"></use>
585 <li className="list-inline-item">
587 class="btn btn-sm btn-link btn-animate text-muted"
588 onClick={linkEvent(this, this.handleDeleteClick)}
596 class={`icon icon-inline ${
597 post.deleted && 'text-danger'
600 <use xlinkHref="#icon-trash"></use>
607 {!this.state.showAdvanced && this.props.showBody ? (
608 <li className="list-inline-item">
610 class="btn btn-sm btn-link btn-animate text-muted"
611 onClick={linkEvent(this, this.handleShowAdvanced)}
612 data-tippy-content={i18n.t('more')}
614 <svg class="icon icon-inline">
615 <use xlinkHref="#icon-more-vertical"></use>
621 {this.props.showBody && post.body && (
622 <li className="list-inline-item">
624 class="btn btn-sm btn-link btn-animate text-muted"
625 onClick={linkEvent(this, this.handleViewSource)}
626 data-tippy-content={i18n.t('view_source')}
629 class={`icon icon-inline ${
630 this.state.viewSource && 'text-success'
633 <use xlinkHref="#icon-file-text"></use>
638 {this.canModOnSelf && (
640 <li className="list-inline-item">
642 class="btn btn-sm btn-link btn-animate text-muted"
643 onClick={linkEvent(this, this.handleModLock)}
651 class={`icon icon-inline ${
652 post.locked && 'text-danger'
655 <use xlinkHref="#icon-lock"></use>
659 <li className="list-inline-item">
661 class="btn btn-sm btn-link btn-animate text-muted"
662 onClick={linkEvent(this, this.handleModSticky)}
670 class={`icon icon-inline ${
671 post.stickied && 'text-success'
674 <use xlinkHref="#icon-pin"></use>
680 {/* Mods can ban from community, and appoint as mods to community */}
681 {(this.canMod || this.canAdmin) && (
682 <li className="list-inline-item">
688 this.handleModRemoveShow
698 this.handleModRemoveSubmit
709 <li className="list-inline-item">
710 {!post.banned_from_community ? (
715 this.handleModBanFromCommunityShow
725 this.handleModBanFromCommunitySubmit
733 {!post.banned_from_community && (
734 <li className="list-inline-item">
739 this.handleAddModToCommunity
743 ? i18n.t('remove_as_mod')
744 : i18n.t('appoint_as_mod')}
750 {/* Community creators and admins can transfer community to another mod */}
751 {(this.amCommunityCreator || this.canAdmin) &&
753 <li className="list-inline-item">
754 {!this.state.showConfirmTransferCommunity ? (
759 this.handleShowConfirmTransferCommunity
762 {i18n.t('transfer_community')}
766 <span class="d-inline-block mr-1">
767 {i18n.t('are_you_sure')}
770 class="pointer d-inline-block mr-1"
773 this.handleTransferCommunity
779 class="pointer d-inline-block"
783 .handleCancelShowConfirmTransferCommunity
792 {/* Admins can ban from all, and appoint other admins */}
796 <li className="list-inline-item">
802 this.handleModBanShow
805 {i18n.t('ban_from_site')}
812 this.handleModBanSubmit
815 {i18n.t('unban_from_site')}
821 <li className="list-inline-item">
824 onClick={linkEvent(this, this.handleAddAdmin)}
827 ? i18n.t('remove_as_admin')
828 : i18n.t('appoint_as_admin')}
834 {/* Site Creator can transfer to another admin */}
835 {this.amSiteCreator && this.isAdmin && (
836 <li className="list-inline-item">
837 {!this.state.showConfirmTransferSite ? (
842 this.handleShowConfirmTransferSite
845 {i18n.t('transfer_site')}
849 <span class="d-inline-block mr-1">
850 {i18n.t('are_you_sure')}
853 class="pointer d-inline-block mr-1"
856 this.handleTransferSite
862 class="pointer d-inline-block"
865 this.handleCancelShowConfirmTransferSite
879 {this.state.showRemoveDialog && (
882 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
886 class="form-control mr-2"
887 placeholder={i18n.t('reason')}
888 value={this.state.removeReason}
889 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
891 <button type="submit" class="btn btn-secondary">
892 {i18n.t('remove_post')}
896 {this.state.showBanDialog && (
897 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
898 <div class="form-group row">
899 <label class="col-form-label" htmlFor="post-listing-reason">
904 id="post-listing-reason"
905 class="form-control mr-2"
906 placeholder={i18n.t('reason')}
907 value={this.state.banReason}
908 onInput={linkEvent(this, this.handleModBanReasonChange)}
911 {/* TODO hold off on expires until later */}
912 {/* <div class="form-group row"> */}
913 {/* <label class="col-form-label">Expires</label> */}
914 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
916 <div class="form-group row">
917 <button type="submit" class="btn btn-secondary">
918 {i18n.t('ban')} {post.creator_name}
930 private get myPost(): boolean {
932 UserService.Instance.user &&
933 this.props.post.creator_id == UserService.Instance.user.id
937 get isMod(): boolean {
939 this.props.moderators &&
941 this.props.moderators.map(m => m.user_id),
942 this.props.post.creator_id
947 get isAdmin(): boolean {
951 this.props.admins.map(a => a.id),
952 this.props.post.creator_id
957 get canMod(): boolean {
958 if (this.props.admins && this.props.moderators) {
959 let adminsThenMods = this.props.admins
961 .concat(this.props.moderators.map(m => m.user_id));
964 UserService.Instance.user,
966 this.props.post.creator_id
973 get canModOnSelf(): boolean {
974 if (this.props.admins && this.props.moderators) {
975 let adminsThenMods = this.props.admins
977 .concat(this.props.moderators.map(m => m.user_id));
980 UserService.Instance.user,
982 this.props.post.creator_id,
990 get canAdmin(): boolean {
994 UserService.Instance.user,
995 this.props.admins.map(a => a.id),
996 this.props.post.creator_id
1001 get amCommunityCreator(): boolean {
1003 this.props.moderators &&
1004 UserService.Instance.user &&
1005 this.props.post.creator_id != UserService.Instance.user.id &&
1006 UserService.Instance.user.id == this.props.moderators[0].user_id
1010 get amSiteCreator(): boolean {
1012 this.props.admins &&
1013 UserService.Instance.user &&
1014 this.props.post.creator_id != UserService.Instance.user.id &&
1015 UserService.Instance.user.id == this.props.admins[0].id
1019 handlePostLike(i: PostListing) {
1020 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1022 if (i.state.my_vote == 1) {
1025 } else if (i.state.my_vote == -1) {
1026 i.state.downvotes--;
1034 i.state.my_vote = new_vote;
1036 let form: CreatePostLikeForm = {
1037 post_id: i.props.post.id,
1038 score: i.state.my_vote,
1041 WebSocketService.Instance.likePost(form);
1042 i.setState(i.state);
1046 handlePostDisLike(i: PostListing) {
1047 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1049 if (i.state.my_vote == 1) {
1052 i.state.downvotes++;
1053 } else if (i.state.my_vote == -1) {
1054 i.state.downvotes--;
1057 i.state.downvotes++;
1061 i.state.my_vote = new_vote;
1063 let form: CreatePostLikeForm = {
1064 post_id: i.props.post.id,
1065 score: i.state.my_vote,
1068 WebSocketService.Instance.likePost(form);
1069 i.setState(i.state);
1073 handleEditClick(i: PostListing) {
1074 i.state.showEdit = true;
1075 i.setState(i.state);
1078 handleEditCancel() {
1079 this.state.showEdit = false;
1080 this.setState(this.state);
1083 // The actual editing is done in the recieve for post
1085 this.state.showEdit = false;
1086 this.setState(this.state);
1089 handleDeleteClick(i: PostListing) {
1090 let deleteForm: PostFormI = {
1091 body: i.props.post.body,
1092 community_id: i.props.post.community_id,
1093 name: i.props.post.name,
1094 url: i.props.post.url,
1095 edit_id: i.props.post.id,
1096 creator_id: i.props.post.creator_id,
1097 deleted: !i.props.post.deleted,
1098 nsfw: i.props.post.nsfw,
1101 WebSocketService.Instance.editPost(deleteForm);
1104 handleSavePostClick(i: PostListing) {
1105 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
1106 let form: SavePostForm = {
1107 post_id: i.props.post.id,
1111 WebSocketService.Instance.savePost(form);
1114 get crossPostParams(): string {
1115 let params = `?title=${this.props.post.name}`;
1116 let post = this.props.post;
1119 params += `&url=${post.url}`;
1121 if (this.props.post.body) {
1122 params += `&body=${this.props.post.body}`;
1127 handleModRemoveShow(i: PostListing) {
1128 i.state.showRemoveDialog = true;
1129 i.setState(i.state);
1132 handleModRemoveReasonChange(i: PostListing, event: any) {
1133 i.state.removeReason = event.target.value;
1134 i.setState(i.state);
1137 handleModRemoveSubmit(i: PostListing) {
1138 event.preventDefault();
1139 let form: PostFormI = {
1140 name: i.props.post.name,
1141 community_id: i.props.post.community_id,
1142 edit_id: i.props.post.id,
1143 creator_id: i.props.post.creator_id,
1144 removed: !i.props.post.removed,
1145 reason: i.state.removeReason,
1146 nsfw: i.props.post.nsfw,
1149 WebSocketService.Instance.editPost(form);
1151 i.state.showRemoveDialog = false;
1152 i.setState(i.state);
1155 handleModLock(i: PostListing) {
1156 let form: PostFormI = {
1157 name: i.props.post.name,
1158 community_id: i.props.post.community_id,
1159 edit_id: i.props.post.id,
1160 creator_id: i.props.post.creator_id,
1161 nsfw: i.props.post.nsfw,
1162 locked: !i.props.post.locked,
1165 WebSocketService.Instance.editPost(form);
1168 handleModSticky(i: PostListing) {
1169 let form: PostFormI = {
1170 name: i.props.post.name,
1171 community_id: i.props.post.community_id,
1172 edit_id: i.props.post.id,
1173 creator_id: i.props.post.creator_id,
1174 nsfw: i.props.post.nsfw,
1175 stickied: !i.props.post.stickied,
1178 WebSocketService.Instance.editPost(form);
1181 handleModBanFromCommunityShow(i: PostListing) {
1182 i.state.showBanDialog = true;
1183 i.state.banType = BanType.Community;
1184 i.setState(i.state);
1187 handleModBanShow(i: PostListing) {
1188 i.state.showBanDialog = true;
1189 i.state.banType = BanType.Site;
1190 i.setState(i.state);
1193 handleModBanReasonChange(i: PostListing, event: any) {
1194 i.state.banReason = event.target.value;
1195 i.setState(i.state);
1198 handleModBanExpiresChange(i: PostListing, event: any) {
1199 i.state.banExpires = event.target.value;
1200 i.setState(i.state);
1203 handleModBanFromCommunitySubmit(i: PostListing) {
1204 i.state.banType = BanType.Community;
1205 i.setState(i.state);
1206 i.handleModBanBothSubmit(i);
1209 handleModBanSubmit(i: PostListing) {
1210 i.state.banType = BanType.Site;
1211 i.setState(i.state);
1212 i.handleModBanBothSubmit(i);
1215 handleModBanBothSubmit(i: PostListing) {
1216 event.preventDefault();
1218 if (i.state.banType == BanType.Community) {
1219 let form: BanFromCommunityForm = {
1220 user_id: i.props.post.creator_id,
1221 community_id: i.props.post.community_id,
1222 ban: !i.props.post.banned_from_community,
1223 reason: i.state.banReason,
1224 expires: getUnixTime(i.state.banExpires),
1226 WebSocketService.Instance.banFromCommunity(form);
1228 let form: BanUserForm = {
1229 user_id: i.props.post.creator_id,
1230 ban: !i.props.post.banned,
1231 reason: i.state.banReason,
1232 expires: getUnixTime(i.state.banExpires),
1234 WebSocketService.Instance.banUser(form);
1237 i.state.showBanDialog = false;
1238 i.setState(i.state);
1241 handleAddModToCommunity(i: PostListing) {
1242 let form: AddModToCommunityForm = {
1243 user_id: i.props.post.creator_id,
1244 community_id: i.props.post.community_id,
1247 WebSocketService.Instance.addModToCommunity(form);
1248 i.setState(i.state);
1251 handleAddAdmin(i: PostListing) {
1252 let form: AddAdminForm = {
1253 user_id: i.props.post.creator_id,
1256 WebSocketService.Instance.addAdmin(form);
1257 i.setState(i.state);
1260 handleShowConfirmTransferCommunity(i: PostListing) {
1261 i.state.showConfirmTransferCommunity = true;
1262 i.setState(i.state);
1265 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1266 i.state.showConfirmTransferCommunity = false;
1267 i.setState(i.state);
1270 handleTransferCommunity(i: PostListing) {
1271 let form: TransferCommunityForm = {
1272 community_id: i.props.post.community_id,
1273 user_id: i.props.post.creator_id,
1275 WebSocketService.Instance.transferCommunity(form);
1276 i.state.showConfirmTransferCommunity = false;
1277 i.setState(i.state);
1280 handleShowConfirmTransferSite(i: PostListing) {
1281 i.state.showConfirmTransferSite = true;
1282 i.setState(i.state);
1285 handleCancelShowConfirmTransferSite(i: PostListing) {
1286 i.state.showConfirmTransferSite = false;
1287 i.setState(i.state);
1290 handleTransferSite(i: PostListing) {
1291 let form: TransferSiteForm = {
1292 user_id: i.props.post.creator_id,
1294 WebSocketService.Instance.transferSite(form);
1295 i.state.showConfirmTransferSite = false;
1296 i.setState(i.state);
1299 handleImageExpandClick(i: PostListing) {
1300 i.state.imageExpanded = !i.state.imageExpanded;
1301 i.setState(i.state);
1304 handleViewSource(i: PostListing) {
1305 i.state.viewSource = !i.state.viewSource;
1306 i.setState(i.state);
1309 handleShowAdvanced(i: PostListing) {
1310 i.state.showAdvanced = !i.state.showAdvanced;
1311 i.setState(i.state);
1315 get pointsTippy(): string {
1316 let points = i18n.t('number_of_points', {
1317 count: this.state.score,
1320 let upvotes = i18n.t('number_of_upvotes', {
1321 count: this.state.upvotes,
1324 let downvotes = i18n.t('number_of_downvotes', {
1325 count: this.state.downvotes,
1328 return `${points} • ${upvotes} • ${downvotes}`;