1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
4 CommentNode as CommentNodeI,
6 CommentForm as CommentFormI,
13 AddModToCommunityForm,
15 TransferCommunityForm,
20 } from '../interfaces';
21 import { WebSocketService, UserService } from '../services';
31 import moment from 'moment';
32 import { MomentTime } from './moment-time';
33 import { CommentForm } from './comment-form';
34 import { CommentNodes } from './comment-nodes';
35 import { UserListing } from './user-listing';
36 import { i18n } from '../i18next';
37 import { CakeDay } from './cake-day';
39 interface CommentNodeState {
42 showRemoveDialog: boolean;
44 showBanDialog: boolean;
48 showConfirmTransferSite: boolean;
49 showConfirmTransferCommunity: boolean;
50 showConfirmAppointAsMod: boolean;
51 showConfirmAppointAsAdmin: boolean;
54 showAdvanced: boolean;
64 interface CommentNodeProps {
70 showContext?: boolean;
71 moderators: Array<CommunityUser>;
72 admins: Array<UserView>;
73 // TODO is this necessary, can't I get it from the node itself?
74 postCreatorId?: number;
75 showCommunity?: boolean;
76 sort?: CommentSortType;
78 enableDownvotes: boolean;
81 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
82 private emptyState: CommentNodeState = {
85 showRemoveDialog: false,
90 banType: BanType.Community,
94 showConfirmTransferSite: false,
95 showConfirmTransferCommunity: false,
96 showConfirmAppointAsMod: false,
97 showConfirmAppointAsAdmin: false,
98 my_vote: this.props.node.comment.my_vote,
99 score: this.props.node.comment.score,
100 upvotes: this.props.node.comment.upvotes,
101 downvotes: this.props.node.comment.downvotes,
102 borderColor: this.props.node.comment.depth
103 ? colorList[this.props.node.comment.depth % colorList.length]
109 constructor(props: any, context: any) {
110 super(props, context);
112 this.state = this.emptyState;
113 this.handleReplyCancel = this.handleReplyCancel.bind(this);
114 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
115 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
118 componentWillReceiveProps(nextProps: CommentNodeProps) {
119 this.state.my_vote = nextProps.node.comment.my_vote;
120 this.state.upvotes = nextProps.node.comment.upvotes;
121 this.state.downvotes = nextProps.node.comment.downvotes;
122 this.state.score = nextProps.node.comment.score;
123 this.state.readLoading = false;
124 this.state.saveLoading = false;
125 this.setState(this.state);
129 let node = this.props.node;
132 className={`comment ${
133 node.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
137 id={`comment-${node.comment.id}`}
138 className={`details comment-node border-top border-light py-2 ${
139 this.isCommentNew ? 'mark' : ''
142 !this.props.noIndent &&
143 this.props.node.comment.parent_id &&
144 `border-left: 2px ${this.state.borderColor} solid !important`
149 !this.props.noIndent &&
150 this.props.node.comment.parent_id &&
154 <div class="d-flex flex-wrap align-items-center text-muted small">
158 name: node.comment.creator_name,
159 avatar: node.comment.creator_avatar,
160 id: node.comment.creator_id,
161 local: node.comment.creator_local,
162 actor_id: node.comment.creator_actor_id,
167 {isCakeDay(node.comment.creator_published) && (
168 <CakeDay creator_name={node.comment.creator_name} />
172 <div className="badge badge-light d-none d-sm-inline mr-2">
177 <div className="badge badge-light d-none d-sm-inline mr-2">
181 {this.isPostCreator && (
182 <div className="badge badge-light d-none d-sm-inline mr-2">
186 {(node.comment.banned_from_community || node.comment.banned) && (
187 <div className="badge badge-danger mr-2">
191 {this.props.showCommunity && (
193 <span class="mx-1">{i18n.t('to')}</span>
194 <Link class="mr-2" to={`/c/${node.comment.community_name}`}>
195 {node.comment.community_name}
200 className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"
201 onClick={linkEvent(this, this.handleCommentCollapse)}
203 {this.state.collapsed ? (
204 <svg class="icon icon-inline">
205 <use xlinkHref="#icon-plus-square"></use>
208 <svg class="icon icon-inline">
209 <use xlinkHref="#icon-minus-square"></use>
214 className={`unselectable pointer ${this.scoreColor}`}
215 onClick={linkEvent(node, this.handleCommentUpvote)}
216 data-tippy-content={this.pointsTippy}
218 <svg class="icon icon-inline mr-1">
219 <use xlinkHref="#icon-zap"></use>
221 <span class="mr-1">{this.state.score}</span>
223 <span className="mr-1">•</span>
225 <MomentTime data={node.comment} />
228 {/* end of user row */}
229 {this.state.showEdit && (
233 onReplyCancel={this.handleReplyCancel}
234 disabled={this.props.locked}
237 {!this.state.showEdit && !this.state.collapsed && (
239 {this.state.viewSource ? (
240 <pre>{this.commentUnlessRemoved}</pre>
244 dangerouslySetInnerHTML={mdToHtml(
245 this.commentUnlessRemoved
249 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
250 {this.props.showContext && this.linkBtn}
251 {this.props.markable && (
253 class="btn btn-link btn-animate text-muted"
254 onClick={linkEvent(this, this.handleMarkRead)}
257 ? i18n.t('mark_as_unread')
258 : i18n.t('mark_as_read')
261 {this.state.readLoading ? (
265 class={`icon icon-inline ${
266 node.comment.read && 'text-success'
269 <use xlinkHref="#icon-check"></use>
274 {UserService.Instance.user && !this.props.viewOnly && (
277 className={`btn btn-link btn-animate ${
278 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
280 onClick={linkEvent(node, this.handleCommentUpvote)}
281 data-tippy-content={i18n.t('upvote')}
283 <svg class="icon icon-inline">
284 <use xlinkHref="#icon-arrow-up"></use>
286 {this.state.upvotes !== this.state.score && (
287 <span class="ml-1">{this.state.upvotes}</span>
290 {this.props.enableDownvotes && (
292 className={`btn btn-link btn-animate ${
293 this.state.my_vote == -1
297 onClick={linkEvent(node, this.handleCommentDownvote)}
298 data-tippy-content={i18n.t('downvote')}
300 <svg class="icon icon-inline">
301 <use xlinkHref="#icon-arrow-down"></use>
303 {this.state.upvotes !== this.state.score && (
304 <span class="ml-1">{this.state.downvotes}</span>
309 class="btn btn-link btn-animate text-muted"
310 onClick={linkEvent(this, this.handleReplyClick)}
311 data-tippy-content={i18n.t('reply')}
313 <svg class="icon icon-inline">
314 <use xlinkHref="#icon-reply1"></use>
317 {!this.state.showAdvanced ? (
319 className="btn btn-link btn-animate text-muted"
320 onClick={linkEvent(this, this.handleShowAdvanced)}
321 data-tippy-content={i18n.t('more')}
323 <svg class="icon icon-inline">
324 <use xlinkHref="#icon-more-vertical"></use>
329 {!this.myComment && (
330 <button class="btn btn-link btn-animate">
333 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
334 title={i18n.t('message').toLowerCase()}
337 <use xlinkHref="#icon-mail"></use>
342 {!this.props.showContext && this.linkBtn}
344 class="btn btn-link btn-animate text-muted"
347 this.handleSaveCommentClick
355 {this.state.saveLoading ? (
359 class={`icon icon-inline ${
360 node.comment.saved && 'text-warning'
363 <use xlinkHref="#icon-star"></use>
368 className="btn btn-link btn-animate text-muted"
369 onClick={linkEvent(this, this.handleViewSource)}
370 data-tippy-content={i18n.t('view_source')}
373 class={`icon icon-inline ${
374 this.state.viewSource && 'text-success'
377 <use xlinkHref="#icon-file-text"></use>
383 class="btn btn-link btn-animate text-muted"
384 onClick={linkEvent(this, this.handleEditClick)}
385 data-tippy-content={i18n.t('edit')}
387 <svg class="icon icon-inline">
388 <use xlinkHref="#icon-edit"></use>
392 class="btn btn-link btn-animate text-muted"
395 this.handleDeleteClick
398 !node.comment.deleted
404 class={`icon icon-inline ${
405 node.comment.deleted && 'text-danger'
408 <use xlinkHref="#icon-trash"></use>
413 {/* Admins and mods can remove comments */}
414 {(this.canMod || this.canAdmin) && (
416 {!node.comment.removed ? (
418 class="btn btn-link btn-animate text-muted"
421 this.handleModRemoveShow
428 class="btn btn-link btn-animate text-muted"
431 this.handleModRemoveSubmit
439 {/* Mods can ban from community, and appoint as mods to community */}
443 (!node.comment.banned_from_community ? (
445 class="btn btn-link btn-animate text-muted"
448 this.handleModBanFromCommunityShow
455 class="btn btn-link btn-animate text-muted"
458 this.handleModBanFromCommunitySubmit
464 {!node.comment.banned_from_community &&
465 (!this.state.showConfirmAppointAsMod ? (
467 class="btn btn-link btn-animate text-muted"
470 this.handleShowConfirmAppointAsMod
474 ? i18n.t('remove_as_mod')
475 : i18n.t('appoint_as_mod')}
479 <button class="btn btn-link btn-animate text-muted">
480 {i18n.t('are_you_sure')}
483 class="btn btn-link btn-animate text-muted"
486 this.handleAddModToCommunity
492 class="btn btn-link btn-animate text-muted"
495 this.handleCancelConfirmAppointAsMod
504 {/* Community creators and admins can transfer community to another mod */}
505 {(this.amCommunityCreator || this.canAdmin) &&
507 (!this.state.showConfirmTransferCommunity ? (
509 class="btn btn-link btn-animate text-muted"
512 this.handleShowConfirmTransferCommunity
515 {i18n.t('transfer_community')}
519 <button class="btn btn-link btn-animate text-muted">
520 {i18n.t('are_you_sure')}
523 class="btn btn-link btn-animate text-muted"
526 this.handleTransferCommunity
532 class="btn btn-link btn-animate text-muted"
536 .handleCancelShowConfirmTransferCommunity
543 {/* Admins can ban from all, and appoint other admins */}
547 (!node.comment.banned ? (
549 class="btn btn-link btn-animate text-muted"
552 this.handleModBanShow
555 {i18n.t('ban_from_site')}
559 class="btn btn-link btn-animate text-muted"
562 this.handleModBanSubmit
565 {i18n.t('unban_from_site')}
568 {!node.comment.banned &&
569 (!this.state.showConfirmAppointAsAdmin ? (
571 class="btn btn-link btn-animate text-muted"
574 this.handleShowConfirmAppointAsAdmin
578 ? i18n.t('remove_as_admin')
579 : i18n.t('appoint_as_admin')}
583 <button class="btn btn-link btn-animate text-muted">
584 {i18n.t('are_you_sure')}
587 class="btn btn-link btn-animate text-muted"
596 class="btn btn-link btn-animate text-muted"
599 this.handleCancelConfirmAppointAsAdmin
608 {/* Site Creator can transfer to another admin */}
609 {this.amSiteCreator &&
611 (!this.state.showConfirmTransferSite ? (
613 class="btn btn-link btn-animate text-muted"
616 this.handleShowConfirmTransferSite
619 {i18n.t('transfer_site')}
623 <button class="btn btn-link btn-animate text-muted">
624 {i18n.t('are_you_sure')}
627 class="btn btn-link btn-animate text-muted"
630 this.handleTransferSite
636 class="btn btn-link btn-animate text-muted"
639 this.handleCancelShowConfirmTransferSite
651 {/* end of button group */}
656 {/* end of details */}
657 {this.state.showRemoveDialog && (
660 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
664 class="form-control mr-2"
665 placeholder={i18n.t('reason')}
666 value={this.state.removeReason}
667 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
669 <button type="submit" class="btn btn-secondary">
670 {i18n.t('remove_comment')}
674 {this.state.showBanDialog && (
675 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
676 <div class="form-group row">
677 <label class="col-form-label">{i18n.t('reason')}</label>
680 class="form-control mr-2"
681 placeholder={i18n.t('reason')}
682 value={this.state.banReason}
683 onInput={linkEvent(this, this.handleModBanReasonChange)}
686 {/* TODO hold off on expires until later */}
687 {/* <div class="form-group row"> */}
688 {/* <label class="col-form-label">Expires</label> */}
689 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
691 <div class="form-group row">
692 <button type="submit" class="btn btn-secondary">
693 {i18n.t('ban')} {node.comment.creator_name}
698 {this.state.showReply && (
701 onReplyCancel={this.handleReplyCancel}
702 disabled={this.props.locked}
705 {node.children && !this.state.collapsed && (
707 nodes={node.children}
708 locked={this.props.locked}
709 moderators={this.props.moderators}
710 admins={this.props.admins}
711 postCreatorId={this.props.postCreatorId}
712 sort={this.props.sort}
713 sortType={this.props.sortType}
714 enableDownvotes={this.props.enableDownvotes}
717 {/* A collapsed clearfix */}
718 {this.state.collapsed && <div class="row col-12"></div>}
724 let node = this.props.node;
727 class="btn btn-link btn-animate text-muted"
728 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
729 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
731 <svg class="icon icon-inline">
732 <use xlinkHref="#icon-link"></use>
740 <svg class="icon icon-spinner spin">
741 <use xlinkHref="#icon-spinner"></use>
746 get myComment(): boolean {
748 UserService.Instance.user &&
749 this.props.node.comment.creator_id == UserService.Instance.user.id
753 get isMod(): boolean {
755 this.props.moderators &&
757 this.props.moderators.map(m => m.user_id),
758 this.props.node.comment.creator_id
763 get isAdmin(): boolean {
767 this.props.admins.map(a => a.id),
768 this.props.node.comment.creator_id
773 get isPostCreator(): boolean {
774 return this.props.node.comment.creator_id == this.props.postCreatorId;
777 get canMod(): boolean {
778 if (this.props.admins && this.props.moderators) {
779 let adminsThenMods = this.props.admins
781 .concat(this.props.moderators.map(m => m.user_id));
784 UserService.Instance.user,
786 this.props.node.comment.creator_id
793 get canAdmin(): boolean {
797 UserService.Instance.user,
798 this.props.admins.map(a => a.id),
799 this.props.node.comment.creator_id
804 get amCommunityCreator(): boolean {
806 this.props.moderators &&
807 UserService.Instance.user &&
808 this.props.node.comment.creator_id != UserService.Instance.user.id &&
809 UserService.Instance.user.id == this.props.moderators[0].user_id
813 get amSiteCreator(): boolean {
816 UserService.Instance.user &&
817 this.props.node.comment.creator_id != UserService.Instance.user.id &&
818 UserService.Instance.user.id == this.props.admins[0].id
822 get commentUnlessRemoved(): string {
823 let node = this.props.node;
824 return node.comment.removed
825 ? `*${i18n.t('removed')}*`
826 : node.comment.deleted
827 ? `*${i18n.t('deleted')}*`
828 : node.comment.content;
831 handleReplyClick(i: CommentNode) {
832 i.state.showReply = true;
836 handleEditClick(i: CommentNode) {
837 i.state.showEdit = true;
841 handleDeleteClick(i: CommentNode) {
842 let deleteForm: CommentFormI = {
843 content: i.props.node.comment.content,
844 edit_id: i.props.node.comment.id,
845 creator_id: i.props.node.comment.creator_id,
846 post_id: i.props.node.comment.post_id,
847 parent_id: i.props.node.comment.parent_id,
848 deleted: !i.props.node.comment.deleted,
851 WebSocketService.Instance.editComment(deleteForm);
854 handleSaveCommentClick(i: CommentNode) {
856 i.props.node.comment.saved == undefined
858 : !i.props.node.comment.saved;
859 let form: SaveCommentForm = {
860 comment_id: i.props.node.comment.id,
864 WebSocketService.Instance.saveComment(form);
866 i.state.saveLoading = true;
867 i.setState(this.state);
870 handleReplyCancel() {
871 this.state.showReply = false;
872 this.state.showEdit = false;
873 this.setState(this.state);
876 handleCommentUpvote(i: CommentNodeI) {
877 let new_vote = this.state.my_vote == 1 ? 0 : 1;
879 if (this.state.my_vote == 1) {
881 this.state.upvotes--;
882 } else if (this.state.my_vote == -1) {
883 this.state.downvotes--;
884 this.state.upvotes++;
885 this.state.score += 2;
887 this.state.upvotes++;
891 this.state.my_vote = new_vote;
893 let form: CommentLikeForm = {
894 comment_id: i.comment.id,
895 post_id: i.comment.post_id,
896 score: this.state.my_vote,
899 WebSocketService.Instance.likeComment(form);
900 this.setState(this.state);
904 handleCommentDownvote(i: CommentNodeI) {
905 let new_vote = this.state.my_vote == -1 ? 0 : -1;
907 if (this.state.my_vote == 1) {
908 this.state.score -= 2;
909 this.state.upvotes--;
910 this.state.downvotes++;
911 } else if (this.state.my_vote == -1) {
912 this.state.downvotes--;
915 this.state.downvotes++;
919 this.state.my_vote = new_vote;
921 let form: CommentLikeForm = {
922 comment_id: i.comment.id,
923 post_id: i.comment.post_id,
924 score: this.state.my_vote,
927 WebSocketService.Instance.likeComment(form);
928 this.setState(this.state);
932 handleModRemoveShow(i: CommentNode) {
933 i.state.showRemoveDialog = true;
937 handleModRemoveReasonChange(i: CommentNode, event: any) {
938 i.state.removeReason = event.target.value;
942 handleModRemoveSubmit(i: CommentNode) {
943 event.preventDefault();
944 let form: CommentFormI = {
945 content: i.props.node.comment.content,
946 edit_id: i.props.node.comment.id,
947 creator_id: i.props.node.comment.creator_id,
948 post_id: i.props.node.comment.post_id,
949 parent_id: i.props.node.comment.parent_id,
950 removed: !i.props.node.comment.removed,
951 reason: i.state.removeReason,
954 WebSocketService.Instance.editComment(form);
956 i.state.showRemoveDialog = false;
960 handleMarkRead(i: CommentNode) {
961 // if it has a user_mention_id field, then its a mention
962 if (i.props.node.comment.user_mention_id) {
963 let form: EditUserMentionForm = {
964 user_mention_id: i.props.node.comment.user_mention_id,
965 read: !i.props.node.comment.read,
967 WebSocketService.Instance.editUserMention(form);
969 let form: CommentFormI = {
970 content: i.props.node.comment.content,
971 edit_id: i.props.node.comment.id,
972 creator_id: i.props.node.comment.creator_id,
973 post_id: i.props.node.comment.post_id,
974 parent_id: i.props.node.comment.parent_id,
975 read: !i.props.node.comment.read,
978 WebSocketService.Instance.editComment(form);
981 i.state.readLoading = true;
982 i.setState(this.state);
985 handleModBanFromCommunityShow(i: CommentNode) {
986 i.state.showBanDialog = !i.state.showBanDialog;
987 i.state.banType = BanType.Community;
991 handleModBanShow(i: CommentNode) {
992 i.state.showBanDialog = !i.state.showBanDialog;
993 i.state.banType = BanType.Site;
997 handleModBanReasonChange(i: CommentNode, event: any) {
998 i.state.banReason = event.target.value;
1002 handleModBanExpiresChange(i: CommentNode, event: any) {
1003 i.state.banExpires = event.target.value;
1004 i.setState(i.state);
1007 handleModBanFromCommunitySubmit(i: CommentNode) {
1008 i.state.banType = BanType.Community;
1009 i.setState(i.state);
1010 i.handleModBanBothSubmit(i);
1013 handleModBanSubmit(i: CommentNode) {
1014 i.state.banType = BanType.Site;
1015 i.setState(i.state);
1016 i.handleModBanBothSubmit(i);
1019 handleModBanBothSubmit(i: CommentNode) {
1020 event.preventDefault();
1022 if (i.state.banType == BanType.Community) {
1023 let form: BanFromCommunityForm = {
1024 user_id: i.props.node.comment.creator_id,
1025 community_id: i.props.node.comment.community_id,
1026 ban: !i.props.node.comment.banned_from_community,
1027 reason: i.state.banReason,
1028 expires: getUnixTime(i.state.banExpires),
1030 WebSocketService.Instance.banFromCommunity(form);
1032 let form: BanUserForm = {
1033 user_id: i.props.node.comment.creator_id,
1034 ban: !i.props.node.comment.banned,
1035 reason: i.state.banReason,
1036 expires: getUnixTime(i.state.banExpires),
1038 WebSocketService.Instance.banUser(form);
1041 i.state.showBanDialog = false;
1042 i.setState(i.state);
1045 handleShowConfirmAppointAsMod(i: CommentNode) {
1046 i.state.showConfirmAppointAsMod = true;
1047 i.setState(i.state);
1050 handleCancelConfirmAppointAsMod(i: CommentNode) {
1051 i.state.showConfirmAppointAsMod = false;
1052 i.setState(i.state);
1055 handleAddModToCommunity(i: CommentNode) {
1056 let form: AddModToCommunityForm = {
1057 user_id: i.props.node.comment.creator_id,
1058 community_id: i.props.node.comment.community_id,
1061 WebSocketService.Instance.addModToCommunity(form);
1062 i.state.showConfirmAppointAsMod = false;
1063 i.setState(i.state);
1066 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1067 i.state.showConfirmAppointAsAdmin = true;
1068 i.setState(i.state);
1071 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1072 i.state.showConfirmAppointAsAdmin = false;
1073 i.setState(i.state);
1076 handleAddAdmin(i: CommentNode) {
1077 let form: AddAdminForm = {
1078 user_id: i.props.node.comment.creator_id,
1081 WebSocketService.Instance.addAdmin(form);
1082 i.state.showConfirmAppointAsAdmin = false;
1083 i.setState(i.state);
1086 handleShowConfirmTransferCommunity(i: CommentNode) {
1087 i.state.showConfirmTransferCommunity = true;
1088 i.setState(i.state);
1091 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1092 i.state.showConfirmTransferCommunity = false;
1093 i.setState(i.state);
1096 handleTransferCommunity(i: CommentNode) {
1097 let form: TransferCommunityForm = {
1098 community_id: i.props.node.comment.community_id,
1099 user_id: i.props.node.comment.creator_id,
1101 WebSocketService.Instance.transferCommunity(form);
1102 i.state.showConfirmTransferCommunity = false;
1103 i.setState(i.state);
1106 handleShowConfirmTransferSite(i: CommentNode) {
1107 i.state.showConfirmTransferSite = true;
1108 i.setState(i.state);
1111 handleCancelShowConfirmTransferSite(i: CommentNode) {
1112 i.state.showConfirmTransferSite = false;
1113 i.setState(i.state);
1116 handleTransferSite(i: CommentNode) {
1117 let form: TransferSiteForm = {
1118 user_id: i.props.node.comment.creator_id,
1120 WebSocketService.Instance.transferSite(form);
1121 i.state.showConfirmTransferSite = false;
1122 i.setState(i.state);
1125 get isCommentNew(): boolean {
1126 let now = moment.utc().subtract(10, 'minutes');
1127 let then = moment.utc(this.props.node.comment.published);
1128 return now.isBefore(then);
1131 handleCommentCollapse(i: CommentNode) {
1132 i.state.collapsed = !i.state.collapsed;
1133 i.setState(i.state);
1136 handleViewSource(i: CommentNode) {
1137 i.state.viewSource = !i.state.viewSource;
1138 i.setState(i.state);
1141 handleShowAdvanced(i: CommentNode) {
1142 i.state.showAdvanced = !i.state.showAdvanced;
1143 i.setState(i.state);
1148 if (this.state.my_vote == 1) {
1150 } else if (this.state.my_vote == -1) {
1151 return 'text-danger';
1153 return 'text-muted';
1157 get pointsTippy(): string {
1158 let points = i18n.t('number_of_points', {
1159 count: this.state.score,
1162 let upvotes = i18n.t('number_of_upvotes', {
1163 count: this.state.upvotes,
1166 let downvotes = i18n.t('number_of_downvotes', {
1167 count: this.state.downvotes,
1170 return `${points} • ${upvotes} • ${downvotes}`;