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';
27 pictshareAvatarThumbnail,
32 import moment from 'moment';
33 import { MomentTime } from './moment-time';
34 import { CommentForm } from './comment-form';
35 import { CommentNodes } from './comment-nodes';
36 import { i18n } from '../i18next';
38 interface CommentNodeState {
41 showRemoveDialog: boolean;
43 showBanDialog: boolean;
47 showConfirmTransferSite: boolean;
48 showConfirmTransferCommunity: boolean;
49 showConfirmAppointAsMod: boolean;
50 showConfirmAppointAsAdmin: boolean;
53 showAdvanced: boolean;
63 interface CommentNodeProps {
69 showContext?: boolean;
70 moderators: Array<CommunityUser>;
71 admins: Array<UserView>;
72 // TODO is this necessary, can't I get it from the node itself?
73 postCreatorId?: number;
74 showCommunity?: boolean;
75 sort?: CommentSortType;
79 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
80 private emptyState: CommentNodeState = {
83 showRemoveDialog: false,
88 banType: BanType.Community,
92 showConfirmTransferSite: false,
93 showConfirmTransferCommunity: false,
94 showConfirmAppointAsMod: false,
95 showConfirmAppointAsAdmin: false,
96 my_vote: this.props.node.comment.my_vote,
97 score: this.props.node.comment.score,
98 upvotes: this.props.node.comment.upvotes,
99 downvotes: this.props.node.comment.downvotes,
100 borderColor: this.props.node.comment.depth
101 ? colorList[this.props.node.comment.depth % colorList.length]
107 constructor(props: any, context: any) {
108 super(props, context);
110 this.state = this.emptyState;
111 this.handleReplyCancel = this.handleReplyCancel.bind(this);
112 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
113 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
116 componentWillReceiveProps(nextProps: CommentNodeProps) {
117 this.state.my_vote = nextProps.node.comment.my_vote;
118 this.state.upvotes = nextProps.node.comment.upvotes;
119 this.state.downvotes = nextProps.node.comment.downvotes;
120 this.state.score = nextProps.node.comment.score;
121 this.state.readLoading = false;
122 this.state.saveLoading = false;
123 this.setState(this.state);
127 let node = this.props.node;
130 className={`comment ${
131 node.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
135 id={`comment-${node.comment.id}`}
136 className={`details comment-node border-top border-light ${
137 this.isCommentNew ? 'mark' : ''
140 !this.props.noIndent &&
141 this.props.node.comment.parent_id &&
142 `border-left: 2px ${this.state.borderColor} solid !important`
146 class={`${!this.props.noIndent &&
147 this.props.node.comment.parent_id &&
150 <div class="d-flex align-items-center mb-1 mt-1 text-muted small">
152 className="mr-2 text-body font-weight-bold"
153 to={`/u/${node.comment.creator_name}`}
155 {node.comment.creator_avatar && showAvatars() && (
159 src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
160 class="rounded-circle mr-1"
163 <span>{node.comment.creator_name}</span>
166 <div className="badge badge-light d-none d-sm-inline mr-2">
171 <div className="badge badge-light d-none d-sm-inline mr-2">
175 {this.isPostCreator && (
176 <div className="badge badge-light d-none d-sm-inline mr-2">
180 {(node.comment.banned_from_community || node.comment.banned) && (
181 <div className="badge badge-danger mr-2">
185 {this.props.showCommunity && (
187 <span class="mx-1">{i18n.t('to')}</span>
188 <Link class="mr-2" to={`/c/${node.comment.community_name}`}>
189 {node.comment.community_name}
194 className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mr-2"
195 onClick={linkEvent(this, this.handleCommentCollapse)}
197 {this.state.collapsed ? (
198 <svg class="icon icon-inline">
199 <use xlinkHref="#icon-plus-square"></use>
202 <svg class="icon icon-inline">
203 <use xlinkHref="#icon-minus-square"></use>
208 className={`unselectable pointer ${this.scoreColor}`}
209 onClick={linkEvent(node, this.handleCommentUpvote)}
210 data-tippy-content={this.pointsTippy}
212 <svg class="icon icon-inline mr-1">
213 <use xlinkHref="#icon-zap"></use>
215 <span class="mr-1">{this.state.score}</span>
217 <span className="mr-1">•</span>
219 <MomentTime data={node.comment} />
222 {/* end of user row */}
223 {this.state.showEdit && (
227 onReplyCancel={this.handleReplyCancel}
228 disabled={this.props.locked}
231 {!this.state.showEdit && !this.state.collapsed && (
233 {this.state.viewSource ? (
234 <pre>{this.commentUnlessRemoved}</pre>
238 dangerouslySetInnerHTML={mdToHtml(
239 this.commentUnlessRemoved
243 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
244 {this.props.showContext && this.linkBtn}
245 {this.props.markable && (
247 class="btn btn-link btn-animate text-muted"
248 onClick={linkEvent(this, this.handleMarkRead)}
251 ? i18n.t('mark_as_unread')
252 : i18n.t('mark_as_read')
255 {this.state.readLoading ? (
259 class={`icon icon-inline ${node.comment.read &&
262 <use xlinkHref="#icon-check"></use>
267 {UserService.Instance.user && !this.props.viewOnly && (
270 className={`btn btn-link btn-animate ${
271 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
273 onClick={linkEvent(node, this.handleCommentUpvote)}
274 data-tippy-content={i18n.t('upvote')}
276 <svg class="icon icon-inline">
277 <use xlinkHref="#icon-arrow-up"></use>
279 {this.state.upvotes !== this.state.score && (
280 <span class="ml-1">{this.state.upvotes}</span>
283 {WebSocketService.Instance.site.enable_downvotes && (
285 className={`btn btn-link btn-animate ${
286 this.state.my_vote == -1
290 onClick={linkEvent(node, this.handleCommentDownvote)}
291 data-tippy-content={i18n.t('downvote')}
293 <svg class="icon icon-inline">
294 <use xlinkHref="#icon-arrow-down"></use>
296 {this.state.upvotes !== this.state.score && (
297 <span class="ml-1">{this.state.downvotes}</span>
302 class="btn btn-link btn-animate text-muted"
303 onClick={linkEvent(this, this.handleSaveCommentClick)}
305 node.comment.saved ? i18n.t('unsave') : i18n.t('save')
308 {this.state.saveLoading ? (
312 class={`icon icon-inline ${node.comment.saved &&
315 <use xlinkHref="#icon-star"></use>
320 class="btn btn-link btn-animate text-muted"
321 onClick={linkEvent(this, this.handleReplyClick)}
322 data-tippy-content={i18n.t('reply')}
324 <svg class="icon icon-inline">
325 <use xlinkHref="#icon-reply1"></use>
328 {!this.state.showAdvanced ? (
330 className="btn btn-link btn-animate text-muted"
331 onClick={linkEvent(this, this.handleShowAdvanced)}
332 data-tippy-content={i18n.t('more')}
334 <svg class="icon icon-inline">
335 <use xlinkHref="#icon-more-vertical"></use>
340 {!this.myComment && (
341 <button class="btn btn-link btn-animate">
344 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
345 title={i18n.t('message').toLowerCase()}
348 <use xlinkHref="#icon-mail"></use>
353 {!this.props.showContext && this.linkBtn}
355 className="btn btn-link btn-animate text-muted"
356 onClick={linkEvent(this, this.handleViewSource)}
357 data-tippy-content={i18n.t('view_source')}
360 class={`icon icon-inline ${this.state
361 .viewSource && 'text-success'}`}
363 <use xlinkHref="#icon-file-text"></use>
369 class="btn btn-link btn-animate text-muted"
370 onClick={linkEvent(this, this.handleEditClick)}
371 data-tippy-content={i18n.t('edit')}
373 <svg class="icon icon-inline">
374 <use xlinkHref="#icon-edit"></use>
378 class="btn btn-link btn-animate text-muted"
381 this.handleDeleteClick
384 !node.comment.deleted
390 class={`icon icon-inline ${node.comment
391 .deleted && 'text-danger'}`}
393 <use xlinkHref="#icon-trash"></use>
398 {/* Admins and mods can remove comments */}
399 {(this.canMod || this.canAdmin) && (
401 {!node.comment.removed ? (
403 class="btn btn-link btn-animate text-muted"
406 this.handleModRemoveShow
413 class="btn btn-link btn-animate text-muted"
416 this.handleModRemoveSubmit
424 {/* Mods can ban from community, and appoint as mods to community */}
428 (!node.comment.banned_from_community ? (
430 class="btn btn-link btn-animate text-muted"
433 this.handleModBanFromCommunityShow
440 class="btn btn-link btn-animate text-muted"
443 this.handleModBanFromCommunitySubmit
449 {!node.comment.banned_from_community &&
450 (!this.state.showConfirmAppointAsMod ? (
452 class="btn btn-link btn-animate text-muted"
455 this.handleShowConfirmAppointAsMod
459 ? i18n.t('remove_as_mod')
460 : i18n.t('appoint_as_mod')}
464 <button class="btn btn-link btn-animate text-muted">
465 {i18n.t('are_you_sure')}
468 class="btn btn-link btn-animate text-muted"
471 this.handleAddModToCommunity
477 class="btn btn-link btn-animate text-muted"
480 this.handleCancelConfirmAppointAsMod
489 {/* Community creators and admins can transfer community to another mod */}
490 {(this.amCommunityCreator || this.canAdmin) &&
492 (!this.state.showConfirmTransferCommunity ? (
494 class="btn btn-link btn-animate text-muted"
497 this.handleShowConfirmTransferCommunity
500 {i18n.t('transfer_community')}
504 <button class="btn btn-link btn-animate text-muted">
505 {i18n.t('are_you_sure')}
508 class="btn btn-link btn-animate text-muted"
511 this.handleTransferCommunity
517 class="btn btn-link btn-animate text-muted"
521 .handleCancelShowConfirmTransferCommunity
528 {/* Admins can ban from all, and appoint other admins */}
532 (!node.comment.banned ? (
534 class="btn btn-link btn-animate text-muted"
537 this.handleModBanShow
540 {i18n.t('ban_from_site')}
544 class="btn btn-link btn-animate text-muted"
547 this.handleModBanSubmit
550 {i18n.t('unban_from_site')}
553 {!node.comment.banned &&
554 (!this.state.showConfirmAppointAsAdmin ? (
556 class="btn btn-link btn-animate text-muted"
559 this.handleShowConfirmAppointAsAdmin
563 ? i18n.t('remove_as_admin')
564 : i18n.t('appoint_as_admin')}
568 <button class="btn btn-link btn-animate text-muted">
569 {i18n.t('are_you_sure')}
572 class="btn btn-link btn-animate text-muted"
581 class="btn btn-link btn-animate text-muted"
584 this.handleCancelConfirmAppointAsAdmin
593 {/* Site Creator can transfer to another admin */}
594 {this.amSiteCreator &&
596 (!this.state.showConfirmTransferSite ? (
598 class="btn btn-link btn-animate text-muted"
601 this.handleShowConfirmTransferSite
604 {i18n.t('transfer_site')}
608 <button class="btn btn-link btn-animate text-muted">
609 {i18n.t('are_you_sure')}
612 class="btn btn-link btn-animate text-muted"
615 this.handleTransferSite
621 class="btn btn-link btn-animate text-muted"
624 this.handleCancelShowConfirmTransferSite
636 {/* end of button group */}
641 {/* end of details */}
642 {this.state.showRemoveDialog && (
645 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
649 class="form-control mr-2"
650 placeholder={i18n.t('reason')}
651 value={this.state.removeReason}
652 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
654 <button type="submit" class="btn btn-secondary">
655 {i18n.t('remove_comment')}
659 {this.state.showBanDialog && (
660 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
661 <div class="form-group row">
662 <label class="col-form-label">{i18n.t('reason')}</label>
665 class="form-control mr-2"
666 placeholder={i18n.t('reason')}
667 value={this.state.banReason}
668 onInput={linkEvent(this, this.handleModBanReasonChange)}
671 {/* TODO hold off on expires until later */}
672 {/* <div class="form-group row"> */}
673 {/* <label class="col-form-label">Expires</label> */}
674 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
676 <div class="form-group row">
677 <button type="submit" class="btn btn-secondary">
678 {i18n.t('ban')} {node.comment.creator_name}
683 {this.state.showReply && (
686 onReplyCancel={this.handleReplyCancel}
687 disabled={this.props.locked}
690 {node.children && !this.state.collapsed && (
692 nodes={node.children}
693 locked={this.props.locked}
694 moderators={this.props.moderators}
695 admins={this.props.admins}
696 postCreatorId={this.props.postCreatorId}
697 sort={this.props.sort}
698 sortType={this.props.sortType}
701 {/* A collapsed clearfix */}
702 {this.state.collapsed && <div class="row col-12"></div>}
708 let node = this.props.node;
710 <button className="btn btn-link btn-animate">
713 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
715 this.props.showContext ? i18n.t('show_context') : i18n.t('link')
718 <svg class="icon icon-inline">
719 <use xlinkHref="#icon-link"></use>
728 <svg class="icon icon-spinner spin">
729 <use xlinkHref="#icon-spinner"></use>
734 get myComment(): boolean {
736 UserService.Instance.user &&
737 this.props.node.comment.creator_id == UserService.Instance.user.id
741 get isMod(): boolean {
743 this.props.moderators &&
745 this.props.moderators.map(m => m.user_id),
746 this.props.node.comment.creator_id
751 get isAdmin(): boolean {
755 this.props.admins.map(a => a.id),
756 this.props.node.comment.creator_id
761 get isPostCreator(): boolean {
762 return this.props.node.comment.creator_id == this.props.postCreatorId;
765 get canMod(): boolean {
766 if (this.props.admins && this.props.moderators) {
767 let adminsThenMods = this.props.admins
769 .concat(this.props.moderators.map(m => m.user_id));
772 UserService.Instance.user,
774 this.props.node.comment.creator_id
781 get canAdmin(): boolean {
785 UserService.Instance.user,
786 this.props.admins.map(a => a.id),
787 this.props.node.comment.creator_id
792 get amCommunityCreator(): boolean {
794 this.props.moderators &&
795 UserService.Instance.user &&
796 this.props.node.comment.creator_id != UserService.Instance.user.id &&
797 UserService.Instance.user.id == this.props.moderators[0].user_id
801 get amSiteCreator(): boolean {
804 UserService.Instance.user &&
805 this.props.node.comment.creator_id != UserService.Instance.user.id &&
806 UserService.Instance.user.id == this.props.admins[0].id
810 get commentUnlessRemoved(): string {
811 let node = this.props.node;
812 return node.comment.removed
813 ? `*${i18n.t('removed')}*`
814 : node.comment.deleted
815 ? `*${i18n.t('deleted')}*`
816 : node.comment.content;
819 handleReplyClick(i: CommentNode) {
820 i.state.showReply = true;
824 handleEditClick(i: CommentNode) {
825 i.state.showEdit = true;
829 handleDeleteClick(i: CommentNode) {
830 let deleteForm: CommentFormI = {
831 content: i.props.node.comment.content,
832 edit_id: i.props.node.comment.id,
833 creator_id: i.props.node.comment.creator_id,
834 post_id: i.props.node.comment.post_id,
835 parent_id: i.props.node.comment.parent_id,
836 deleted: !i.props.node.comment.deleted,
839 WebSocketService.Instance.editComment(deleteForm);
842 handleSaveCommentClick(i: CommentNode) {
844 i.props.node.comment.saved == undefined
846 : !i.props.node.comment.saved;
847 let form: SaveCommentForm = {
848 comment_id: i.props.node.comment.id,
852 WebSocketService.Instance.saveComment(form);
854 i.state.saveLoading = true;
855 i.setState(this.state);
858 handleReplyCancel() {
859 this.state.showReply = false;
860 this.state.showEdit = false;
861 this.setState(this.state);
864 handleCommentUpvote(i: CommentNodeI) {
865 let new_vote = this.state.my_vote == 1 ? 0 : 1;
867 if (this.state.my_vote == 1) {
869 this.state.upvotes--;
870 } else if (this.state.my_vote == -1) {
871 this.state.downvotes--;
872 this.state.upvotes++;
873 this.state.score += 2;
875 this.state.upvotes++;
879 this.state.my_vote = new_vote;
881 let form: CommentLikeForm = {
882 comment_id: i.comment.id,
883 post_id: i.comment.post_id,
884 score: this.state.my_vote,
887 WebSocketService.Instance.likeComment(form);
888 this.setState(this.state);
892 handleCommentDownvote(i: CommentNodeI) {
893 let new_vote = this.state.my_vote == -1 ? 0 : -1;
895 if (this.state.my_vote == 1) {
896 this.state.score -= 2;
897 this.state.upvotes--;
898 this.state.downvotes++;
899 } else if (this.state.my_vote == -1) {
900 this.state.downvotes--;
903 this.state.downvotes++;
907 this.state.my_vote = new_vote;
909 let form: CommentLikeForm = {
910 comment_id: i.comment.id,
911 post_id: i.comment.post_id,
912 score: this.state.my_vote,
915 WebSocketService.Instance.likeComment(form);
916 this.setState(this.state);
920 handleModRemoveShow(i: CommentNode) {
921 i.state.showRemoveDialog = true;
925 handleModRemoveReasonChange(i: CommentNode, event: any) {
926 i.state.removeReason = event.target.value;
930 handleModRemoveSubmit(i: CommentNode) {
931 event.preventDefault();
932 let form: CommentFormI = {
933 content: i.props.node.comment.content,
934 edit_id: i.props.node.comment.id,
935 creator_id: i.props.node.comment.creator_id,
936 post_id: i.props.node.comment.post_id,
937 parent_id: i.props.node.comment.parent_id,
938 removed: !i.props.node.comment.removed,
939 reason: i.state.removeReason,
942 WebSocketService.Instance.editComment(form);
944 i.state.showRemoveDialog = false;
948 handleMarkRead(i: CommentNode) {
949 // if it has a user_mention_id field, then its a mention
950 if (i.props.node.comment.user_mention_id) {
951 let form: EditUserMentionForm = {
952 user_mention_id: i.props.node.comment.user_mention_id,
953 read: !i.props.node.comment.read,
955 WebSocketService.Instance.editUserMention(form);
957 let form: CommentFormI = {
958 content: i.props.node.comment.content,
959 edit_id: i.props.node.comment.id,
960 creator_id: i.props.node.comment.creator_id,
961 post_id: i.props.node.comment.post_id,
962 parent_id: i.props.node.comment.parent_id,
963 read: !i.props.node.comment.read,
966 WebSocketService.Instance.editComment(form);
969 i.state.readLoading = true;
970 i.setState(this.state);
973 handleModBanFromCommunityShow(i: CommentNode) {
974 i.state.showBanDialog = !i.state.showBanDialog;
975 i.state.banType = BanType.Community;
979 handleModBanShow(i: CommentNode) {
980 i.state.showBanDialog = !i.state.showBanDialog;
981 i.state.banType = BanType.Site;
985 handleModBanReasonChange(i: CommentNode, event: any) {
986 i.state.banReason = event.target.value;
990 handleModBanExpiresChange(i: CommentNode, event: any) {
991 i.state.banExpires = event.target.value;
995 handleModBanFromCommunitySubmit(i: CommentNode) {
996 i.state.banType = BanType.Community;
998 i.handleModBanBothSubmit(i);
1001 handleModBanSubmit(i: CommentNode) {
1002 i.state.banType = BanType.Site;
1003 i.setState(i.state);
1004 i.handleModBanBothSubmit(i);
1007 handleModBanBothSubmit(i: CommentNode) {
1008 event.preventDefault();
1010 if (i.state.banType == BanType.Community) {
1011 let form: BanFromCommunityForm = {
1012 user_id: i.props.node.comment.creator_id,
1013 community_id: i.props.node.comment.community_id,
1014 ban: !i.props.node.comment.banned_from_community,
1015 reason: i.state.banReason,
1016 expires: getUnixTime(i.state.banExpires),
1018 WebSocketService.Instance.banFromCommunity(form);
1020 let form: BanUserForm = {
1021 user_id: i.props.node.comment.creator_id,
1022 ban: !i.props.node.comment.banned,
1023 reason: i.state.banReason,
1024 expires: getUnixTime(i.state.banExpires),
1026 WebSocketService.Instance.banUser(form);
1029 i.state.showBanDialog = false;
1030 i.setState(i.state);
1033 handleShowConfirmAppointAsMod(i: CommentNode) {
1034 i.state.showConfirmAppointAsMod = true;
1035 i.setState(i.state);
1038 handleCancelConfirmAppointAsMod(i: CommentNode) {
1039 i.state.showConfirmAppointAsMod = false;
1040 i.setState(i.state);
1043 handleAddModToCommunity(i: CommentNode) {
1044 let form: AddModToCommunityForm = {
1045 user_id: i.props.node.comment.creator_id,
1046 community_id: i.props.node.comment.community_id,
1049 WebSocketService.Instance.addModToCommunity(form);
1050 i.state.showConfirmAppointAsMod = false;
1051 i.setState(i.state);
1054 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1055 i.state.showConfirmAppointAsAdmin = true;
1056 i.setState(i.state);
1059 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1060 i.state.showConfirmAppointAsAdmin = false;
1061 i.setState(i.state);
1064 handleAddAdmin(i: CommentNode) {
1065 let form: AddAdminForm = {
1066 user_id: i.props.node.comment.creator_id,
1069 WebSocketService.Instance.addAdmin(form);
1070 i.state.showConfirmAppointAsAdmin = false;
1071 i.setState(i.state);
1074 handleShowConfirmTransferCommunity(i: CommentNode) {
1075 i.state.showConfirmTransferCommunity = true;
1076 i.setState(i.state);
1079 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1080 i.state.showConfirmTransferCommunity = false;
1081 i.setState(i.state);
1084 handleTransferCommunity(i: CommentNode) {
1085 let form: TransferCommunityForm = {
1086 community_id: i.props.node.comment.community_id,
1087 user_id: i.props.node.comment.creator_id,
1089 WebSocketService.Instance.transferCommunity(form);
1090 i.state.showConfirmTransferCommunity = false;
1091 i.setState(i.state);
1094 handleShowConfirmTransferSite(i: CommentNode) {
1095 i.state.showConfirmTransferSite = true;
1096 i.setState(i.state);
1099 handleCancelShowConfirmTransferSite(i: CommentNode) {
1100 i.state.showConfirmTransferSite = false;
1101 i.setState(i.state);
1104 handleTransferSite(i: CommentNode) {
1105 let form: TransferSiteForm = {
1106 user_id: i.props.node.comment.creator_id,
1108 WebSocketService.Instance.transferSite(form);
1109 i.state.showConfirmTransferSite = false;
1110 i.setState(i.state);
1113 get isCommentNew(): boolean {
1114 let now = moment.utc().subtract(10, 'minutes');
1115 let then = moment.utc(this.props.node.comment.published);
1116 return now.isBefore(then);
1119 handleCommentCollapse(i: CommentNode) {
1120 i.state.collapsed = !i.state.collapsed;
1121 i.setState(i.state);
1124 handleViewSource(i: CommentNode) {
1125 i.state.viewSource = !i.state.viewSource;
1126 i.setState(i.state);
1129 handleShowAdvanced(i: CommentNode) {
1130 i.state.showAdvanced = !i.state.showAdvanced;
1131 i.setState(i.state);
1136 if (this.state.my_vote == 1) {
1138 } else if (this.state.my_vote == -1) {
1139 return 'text-danger';
1141 return 'text-muted';
1145 get pointsTippy(): string {
1146 let points = i18n.t('number_of_points', {
1147 count: this.state.score,
1150 let upvotes = i18n.t('number_of_upvotes', {
1151 count: this.state.upvotes,
1154 let downvotes = i18n.t('number_of_downvotes', {
1155 count: this.state.downvotes,
1158 return `${points} • ${upvotes} • ${downvotes}`;