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 <svg class="icon thumbnail">
244 <use xlinkHref="#icon-external-link"></use>
252 className="text-body"
253 to={`/post/${post.id}`}
254 title={i18n.t('comments')}
256 <svg class="icon thumbnail">
257 <use xlinkHref="#icon-message-square"></use>
265 let post = this.props.post;
268 <div className={`vote-bar col-1 pr-0 small text-center`}>
270 className={`btn-animate btn btn-link p-0 ${
271 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
273 onClick={linkEvent(this, this.handlePostLike)}
274 data-tippy-content={i18n.t('upvote')}
276 <svg class="icon upvote">
277 <use xlinkHref="#icon-arrow-up1"></use>
281 class={`unselectable pointer font-weight-bold text-muted px-1`}
282 data-tippy-content={this.pointsTippy}
286 {this.props.enableDownvotes && (
288 className={`btn-animate btn btn-link p-0 ${
289 this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
291 onClick={linkEvent(this, this.handlePostDisLike)}
292 data-tippy-content={i18n.t('downvote')}
294 <svg class="icon downvote">
295 <use xlinkHref="#icon-arrow-down1"></use>
300 {!this.state.imageExpanded && (
301 <div class="col-3 col-sm-2 pr-0 mt-1">
302 <div class="position-relative">{this.thumbnail()}</div>
306 class={`${this.state.imageExpanded ? 'col-12' : 'col-8 col-sm-9'}`}
309 <div className="col-12">
310 <div className="post-title">
311 <h5 className="mb-0 d-inline">
312 {this.props.showBody && post.url ? (
314 className="text-body"
324 className="text-body"
325 to={`/post/${post.id}`}
326 title={i18n.t('comments')}
332 {post.url && !(hostname(post.url) == window.location.hostname) && (
333 <small class="d-inline-block">
335 className="ml-2 text-muted font-italic"
342 <svg class="ml-1 icon icon-inline">
343 <use xlinkHref="#icon-external-link"></use>
348 {(isImage(post.url) || this.props.post.thumbnail_url) && (
350 {!this.state.imageExpanded ? (
352 class="text-monospace unselectable pointer ml-2 text-muted small"
353 data-tippy-content={i18n.t('expand_here')}
354 onClick={linkEvent(this, this.handleImageExpandClick)}
356 <svg class="icon icon-inline">
357 <use xlinkHref="#icon-plus-square"></use>
363 class="text-monospace unselectable pointer ml-2 text-muted small"
364 onClick={linkEvent(this, this.handleImageExpandClick)}
366 <svg class="icon icon-inline">
367 <use xlinkHref="#icon-minus-square"></use>
375 this.handleImageExpandClick
379 class="img-fluid img-expanded"
380 src={this.getImage()}
389 <small className="ml-2 text-muted font-italic">
395 className="unselectable pointer ml-2 text-muted font-italic"
396 data-tippy-content={i18n.t('deleted')}
398 <svg class={`icon icon-inline text-danger`}>
399 <use xlinkHref="#icon-trash"></use>
405 className="unselectable pointer ml-2 text-muted font-italic"
406 data-tippy-content={i18n.t('locked')}
408 <svg class={`icon icon-inline text-danger`}>
409 <use xlinkHref="#icon-lock"></use>
415 className="unselectable pointer ml-2 text-muted font-italic"
416 data-tippy-content={i18n.t('stickied')}
418 <svg class={`icon icon-inline text-success`}>
419 <use xlinkHref="#icon-pin"></use>
424 <small className="ml-2 text-muted font-italic">
432 <div className="details col-12">
433 <ul class="list-inline mb-0 text-muted small">
434 <li className="list-inline-item">
435 <span>{i18n.t('by')} </span>
438 name: post.creator_name,
439 avatar: post.creator_avatar,
441 local: post.creator_local,
442 actor_id: post.creator_actor_id,
443 published: post.creator_published,
448 <span className="mx-1 badge badge-light">
453 <span className="mx-1 badge badge-light">
457 {(post.banned_from_community || post.banned) && (
458 <span className="mx-1 badge badge-danger">
462 {this.props.showCommunity && (
464 <span> {i18n.t('to')} </span>
467 name: post.community_name,
468 id: post.community_id,
469 local: post.community_local,
470 actor_id: post.community_actor_id,
476 <li className="list-inline-item">•</li>
477 <li className="list-inline-item">
479 <MomentTime data={post} />
484 <li className="list-inline-item">•</li>
485 <li className="list-inline-item">
486 {/* Using a link with tippy doesn't work on touch devices unfortunately */}
488 className="text-muted"
489 data-tippy-content={md.render(previewLines(post.body))}
490 data-tippy-allowHtml={true}
491 to={`/post/${post.id}`}
493 <svg class="mr-1 icon icon-inline">
494 <use xlinkHref="#icon-book-open"></use>
500 <li className="list-inline-item">•</li>
501 {this.state.upvotes !== this.state.score && (
504 class="unselectable pointer mr-2"
505 data-tippy-content={this.pointsTippy}
507 <li className="list-inline-item">
508 <span className="text-muted">
509 <svg class="small icon icon-inline mr-1">
510 <use xlinkHref="#icon-arrow-up"></use>
515 <li className="list-inline-item">
516 <span className="text-muted">
517 <svg class="small icon icon-inline mr-1">
518 <use xlinkHref="#icon-arrow-down"></use>
520 {this.state.downvotes}
524 <li className="list-inline-item">•</li>
527 <li className="list-inline-item">
529 className="text-muted"
530 title={i18n.t('number_of_comments', {
531 count: post.number_of_comments,
533 to={`/post/${post.id}`}
535 <svg class="mr-1 icon icon-inline">
536 <use xlinkHref="#icon-message-square"></use>
538 {post.number_of_comments}
542 {this.props.post.duplicates && (
543 <ul class="list-inline mb-1 small text-muted">
545 <li className="list-inline-item mr-2">
546 {i18n.t('cross_posted_to')}
548 {this.props.post.duplicates.map(post => (
549 <li className="list-inline-item mr-2">
550 <Link to={`/post/${post.id}`}>
551 {post.community_name}
558 <ul class="list-inline mb-1 text-muted font-weight-bold">
559 {UserService.Instance.user && (
561 {this.props.showBody && (
563 <li className="list-inline-item">
565 class="btn btn-sm btn-link btn-animate text-muted"
566 onClick={linkEvent(this, this.handleSavePostClick)}
568 post.saved ? i18n.t('unsave') : i18n.t('save')
572 class={`icon icon-inline ${
573 post.saved && 'text-warning'
576 <use xlinkHref="#icon-star"></use>
580 <li className="list-inline-item">
582 class="btn btn-sm btn-link btn-animate text-muted"
583 to={`/create_post${this.crossPostParams}`}
584 title={i18n.t('cross_post')}
586 <svg class="icon icon-inline">
587 <use xlinkHref="#icon-copy"></use>
593 {this.myPost && this.props.showBody && (
595 <li className="list-inline-item">
597 class="btn btn-sm btn-link btn-animate text-muted"
598 onClick={linkEvent(this, this.handleEditClick)}
599 data-tippy-content={i18n.t('edit')}
601 <svg class="icon icon-inline">
602 <use xlinkHref="#icon-edit"></use>
606 <li className="list-inline-item">
608 class="btn btn-sm btn-link btn-animate text-muted"
609 onClick={linkEvent(this, this.handleDeleteClick)}
617 class={`icon icon-inline ${
618 post.deleted && 'text-danger'
621 <use xlinkHref="#icon-trash"></use>
628 {!this.state.showAdvanced && this.props.showBody ? (
629 <li className="list-inline-item">
631 class="btn btn-sm btn-link btn-animate text-muted"
632 onClick={linkEvent(this, this.handleShowAdvanced)}
633 data-tippy-content={i18n.t('more')}
635 <svg class="icon icon-inline">
636 <use xlinkHref="#icon-more-vertical"></use>
642 {this.props.showBody && post.body && (
643 <li className="list-inline-item">
645 class="btn btn-sm btn-link btn-animate text-muted"
646 onClick={linkEvent(this, this.handleViewSource)}
647 data-tippy-content={i18n.t('view_source')}
650 class={`icon icon-inline ${
651 this.state.viewSource && 'text-success'
654 <use xlinkHref="#icon-file-text"></use>
659 {this.canModOnSelf && (
661 <li className="list-inline-item">
663 class="btn btn-sm btn-link btn-animate text-muted"
664 onClick={linkEvent(this, this.handleModLock)}
672 class={`icon icon-inline ${
673 post.locked && 'text-danger'
676 <use xlinkHref="#icon-lock"></use>
680 <li className="list-inline-item">
682 class="btn btn-sm btn-link btn-animate text-muted"
683 onClick={linkEvent(this, this.handleModSticky)}
691 class={`icon icon-inline ${
692 post.stickied && 'text-success'
695 <use xlinkHref="#icon-pin"></use>
701 {/* Mods can ban from community, and appoint as mods to community */}
702 {(this.canMod || this.canAdmin) && (
703 <li className="list-inline-item">
709 this.handleModRemoveShow
719 this.handleModRemoveSubmit
730 <li className="list-inline-item">
731 {!post.banned_from_community ? (
736 this.handleModBanFromCommunityShow
746 this.handleModBanFromCommunitySubmit
754 {!post.banned_from_community && (
755 <li className="list-inline-item">
760 this.handleAddModToCommunity
764 ? i18n.t('remove_as_mod')
765 : i18n.t('appoint_as_mod')}
771 {/* Community creators and admins can transfer community to another mod */}
772 {(this.amCommunityCreator || this.canAdmin) &&
774 <li className="list-inline-item">
775 {!this.state.showConfirmTransferCommunity ? (
780 this.handleShowConfirmTransferCommunity
783 {i18n.t('transfer_community')}
787 <span class="d-inline-block mr-1">
788 {i18n.t('are_you_sure')}
791 class="pointer d-inline-block mr-1"
794 this.handleTransferCommunity
800 class="pointer d-inline-block"
804 .handleCancelShowConfirmTransferCommunity
813 {/* Admins can ban from all, and appoint other admins */}
817 <li className="list-inline-item">
823 this.handleModBanShow
826 {i18n.t('ban_from_site')}
833 this.handleModBanSubmit
836 {i18n.t('unban_from_site')}
842 <li className="list-inline-item">
845 onClick={linkEvent(this, this.handleAddAdmin)}
848 ? i18n.t('remove_as_admin')
849 : i18n.t('appoint_as_admin')}
855 {/* Site Creator can transfer to another admin */}
856 {this.amSiteCreator && this.isAdmin && (
857 <li className="list-inline-item">
858 {!this.state.showConfirmTransferSite ? (
863 this.handleShowConfirmTransferSite
866 {i18n.t('transfer_site')}
870 <span class="d-inline-block mr-1">
871 {i18n.t('are_you_sure')}
874 class="pointer d-inline-block mr-1"
877 this.handleTransferSite
883 class="pointer d-inline-block"
886 this.handleCancelShowConfirmTransferSite
900 {this.state.showRemoveDialog && (
903 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
907 class="form-control mr-2"
908 placeholder={i18n.t('reason')}
909 value={this.state.removeReason}
910 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
912 <button type="submit" class="btn btn-secondary">
913 {i18n.t('remove_post')}
917 {this.state.showBanDialog && (
918 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
919 <div class="form-group row">
920 <label class="col-form-label" htmlFor="post-listing-reason">
925 id="post-listing-reason"
926 class="form-control mr-2"
927 placeholder={i18n.t('reason')}
928 value={this.state.banReason}
929 onInput={linkEvent(this, this.handleModBanReasonChange)}
932 {/* TODO hold off on expires until later */}
933 {/* <div class="form-group row"> */}
934 {/* <label class="col-form-label">Expires</label> */}
935 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
937 <div class="form-group row">
938 <button type="submit" class="btn btn-secondary">
939 {i18n.t('ban')} {post.creator_name}
951 private get myPost(): boolean {
953 UserService.Instance.user &&
954 this.props.post.creator_id == UserService.Instance.user.id
958 get isMod(): boolean {
960 this.props.moderators &&
962 this.props.moderators.map(m => m.user_id),
963 this.props.post.creator_id
968 get isAdmin(): boolean {
972 this.props.admins.map(a => a.id),
973 this.props.post.creator_id
978 get canMod(): boolean {
979 if (this.props.admins && this.props.moderators) {
980 let adminsThenMods = this.props.admins
982 .concat(this.props.moderators.map(m => m.user_id));
985 UserService.Instance.user,
987 this.props.post.creator_id
994 get canModOnSelf(): boolean {
995 if (this.props.admins && this.props.moderators) {
996 let adminsThenMods = this.props.admins
998 .concat(this.props.moderators.map(m => m.user_id));
1001 UserService.Instance.user,
1003 this.props.post.creator_id,
1011 get canAdmin(): boolean {
1013 this.props.admins &&
1015 UserService.Instance.user,
1016 this.props.admins.map(a => a.id),
1017 this.props.post.creator_id
1022 get amCommunityCreator(): boolean {
1024 this.props.moderators &&
1025 UserService.Instance.user &&
1026 this.props.post.creator_id != UserService.Instance.user.id &&
1027 UserService.Instance.user.id == this.props.moderators[0].user_id
1031 get amSiteCreator(): boolean {
1033 this.props.admins &&
1034 UserService.Instance.user &&
1035 this.props.post.creator_id != UserService.Instance.user.id &&
1036 UserService.Instance.user.id == this.props.admins[0].id
1040 handlePostLike(i: PostListing) {
1041 if (!UserService.Instance.user) {
1042 this.context.router.history.push(`/login`);
1045 let new_vote = i.state.my_vote == 1 ? 0 : 1;
1047 if (i.state.my_vote == 1) {
1050 } else if (i.state.my_vote == -1) {
1051 i.state.downvotes--;
1059 i.state.my_vote = new_vote;
1061 let form: CreatePostLikeForm = {
1062 post_id: i.props.post.id,
1063 score: i.state.my_vote,
1066 WebSocketService.Instance.likePost(form);
1067 i.setState(i.state);
1071 handlePostDisLike(i: PostListing) {
1072 if (!UserService.Instance.user) {
1073 this.context.router.history.push(`/login`);
1076 let new_vote = i.state.my_vote == -1 ? 0 : -1;
1078 if (i.state.my_vote == 1) {
1081 i.state.downvotes++;
1082 } else if (i.state.my_vote == -1) {
1083 i.state.downvotes--;
1086 i.state.downvotes++;
1090 i.state.my_vote = new_vote;
1092 let form: CreatePostLikeForm = {
1093 post_id: i.props.post.id,
1094 score: i.state.my_vote,
1097 WebSocketService.Instance.likePost(form);
1098 i.setState(i.state);
1102 handleEditClick(i: PostListing) {
1103 i.state.showEdit = true;
1104 i.setState(i.state);
1107 handleEditCancel() {
1108 this.state.showEdit = false;
1109 this.setState(this.state);
1112 // The actual editing is done in the recieve for post
1114 this.state.showEdit = false;
1115 this.setState(this.state);
1118 handleDeleteClick(i: PostListing) {
1119 let deleteForm: DeletePostForm = {
1120 edit_id: i.props.post.id,
1121 deleted: !i.props.post.deleted,
1124 WebSocketService.Instance.deletePost(deleteForm);
1127 handleSavePostClick(i: PostListing) {
1128 let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
1129 let form: SavePostForm = {
1130 post_id: i.props.post.id,
1134 WebSocketService.Instance.savePost(form);
1137 get crossPostParams(): string {
1138 let params = `?title=${this.props.post.name}`;
1139 let post = this.props.post;
1142 params += `&url=${post.url}`;
1144 if (this.props.post.body) {
1145 params += `&body=${this.props.post.body}`;
1150 handleModRemoveShow(i: PostListing) {
1151 i.state.showRemoveDialog = true;
1152 i.setState(i.state);
1155 handleModRemoveReasonChange(i: PostListing, event: any) {
1156 i.state.removeReason = event.target.value;
1157 i.setState(i.state);
1160 handleModRemoveSubmit(i: PostListing) {
1161 event.preventDefault();
1162 let form: RemovePostForm = {
1163 edit_id: i.props.post.id,
1164 removed: !i.props.post.removed,
1165 reason: i.state.removeReason,
1168 WebSocketService.Instance.removePost(form);
1170 i.state.showRemoveDialog = false;
1171 i.setState(i.state);
1174 handleModLock(i: PostListing) {
1175 let form: LockPostForm = {
1176 edit_id: i.props.post.id,
1177 locked: !i.props.post.locked,
1180 WebSocketService.Instance.lockPost(form);
1183 handleModSticky(i: PostListing) {
1184 let form: StickyPostForm = {
1185 edit_id: i.props.post.id,
1186 stickied: !i.props.post.stickied,
1189 WebSocketService.Instance.stickyPost(form);
1192 handleModBanFromCommunityShow(i: PostListing) {
1193 i.state.showBanDialog = true;
1194 i.state.banType = BanType.Community;
1195 i.setState(i.state);
1198 handleModBanShow(i: PostListing) {
1199 i.state.showBanDialog = true;
1200 i.state.banType = BanType.Site;
1201 i.setState(i.state);
1204 handleModBanReasonChange(i: PostListing, event: any) {
1205 i.state.banReason = event.target.value;
1206 i.setState(i.state);
1209 handleModBanExpiresChange(i: PostListing, event: any) {
1210 i.state.banExpires = event.target.value;
1211 i.setState(i.state);
1214 handleModBanFromCommunitySubmit(i: PostListing) {
1215 i.state.banType = BanType.Community;
1216 i.setState(i.state);
1217 i.handleModBanBothSubmit(i);
1220 handleModBanSubmit(i: PostListing) {
1221 i.state.banType = BanType.Site;
1222 i.setState(i.state);
1223 i.handleModBanBothSubmit(i);
1226 handleModBanBothSubmit(i: PostListing) {
1227 event.preventDefault();
1229 if (i.state.banType == BanType.Community) {
1230 let form: BanFromCommunityForm = {
1231 user_id: i.props.post.creator_id,
1232 community_id: i.props.post.community_id,
1233 ban: !i.props.post.banned_from_community,
1234 reason: i.state.banReason,
1235 expires: getUnixTime(i.state.banExpires),
1237 WebSocketService.Instance.banFromCommunity(form);
1239 let form: BanUserForm = {
1240 user_id: i.props.post.creator_id,
1241 ban: !i.props.post.banned,
1242 reason: i.state.banReason,
1243 expires: getUnixTime(i.state.banExpires),
1245 WebSocketService.Instance.banUser(form);
1248 i.state.showBanDialog = false;
1249 i.setState(i.state);
1252 handleAddModToCommunity(i: PostListing) {
1253 let form: AddModToCommunityForm = {
1254 user_id: i.props.post.creator_id,
1255 community_id: i.props.post.community_id,
1258 WebSocketService.Instance.addModToCommunity(form);
1259 i.setState(i.state);
1262 handleAddAdmin(i: PostListing) {
1263 let form: AddAdminForm = {
1264 user_id: i.props.post.creator_id,
1267 WebSocketService.Instance.addAdmin(form);
1268 i.setState(i.state);
1271 handleShowConfirmTransferCommunity(i: PostListing) {
1272 i.state.showConfirmTransferCommunity = true;
1273 i.setState(i.state);
1276 handleCancelShowConfirmTransferCommunity(i: PostListing) {
1277 i.state.showConfirmTransferCommunity = false;
1278 i.setState(i.state);
1281 handleTransferCommunity(i: PostListing) {
1282 let form: TransferCommunityForm = {
1283 community_id: i.props.post.community_id,
1284 user_id: i.props.post.creator_id,
1286 WebSocketService.Instance.transferCommunity(form);
1287 i.state.showConfirmTransferCommunity = false;
1288 i.setState(i.state);
1291 handleShowConfirmTransferSite(i: PostListing) {
1292 i.state.showConfirmTransferSite = true;
1293 i.setState(i.state);
1296 handleCancelShowConfirmTransferSite(i: PostListing) {
1297 i.state.showConfirmTransferSite = false;
1298 i.setState(i.state);
1301 handleTransferSite(i: PostListing) {
1302 let form: TransferSiteForm = {
1303 user_id: i.props.post.creator_id,
1305 WebSocketService.Instance.transferSite(form);
1306 i.state.showConfirmTransferSite = false;
1307 i.setState(i.state);
1310 handleImageExpandClick(i: PostListing) {
1311 i.state.imageExpanded = !i.state.imageExpanded;
1312 i.setState(i.state);
1315 handleViewSource(i: PostListing) {
1316 i.state.viewSource = !i.state.viewSource;
1317 i.setState(i.state);
1320 handleShowAdvanced(i: PostListing) {
1321 i.state.showAdvanced = !i.state.showAdvanced;
1322 i.setState(i.state);
1326 get pointsTippy(): string {
1327 let points = i18n.t('number_of_points', {
1328 count: this.state.score,
1331 let upvotes = i18n.t('number_of_upvotes', {
1332 count: this.state.upvotes,
1335 let downvotes = i18n.t('number_of_downvotes', {
1336 count: this.state.downvotes,
1339 return `${points} • ${upvotes} • ${downvotes}`;