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';
30 import moment from 'moment';
31 import { MomentTime } from './moment-time';
32 import { CommentForm } from './comment-form';
33 import { CommentNodes } from './comment-nodes';
34 import { UserListing } from './user-listing';
35 import { i18n } from '../i18next';
37 interface CommentNodeState {
40 showRemoveDialog: boolean;
42 showBanDialog: boolean;
46 showConfirmTransferSite: boolean;
47 showConfirmTransferCommunity: boolean;
48 showConfirmAppointAsMod: boolean;
49 showConfirmAppointAsAdmin: boolean;
52 showAdvanced: boolean;
62 interface CommentNodeProps {
68 showContext?: boolean;
69 moderators: Array<CommunityUser>;
70 admins: Array<UserView>;
71 // TODO is this necessary, can't I get it from the node itself?
72 postCreatorId?: number;
73 showCommunity?: boolean;
74 sort?: CommentSortType;
76 enableDownvotes: boolean;
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 py-2 ${
137 this.isCommentNew ? 'mark' : ''
140 !this.props.noIndent &&
141 this.props.node.comment.parent_id &&
142 `border-left: 2px ${this.state.borderColor} solid !important`
147 !this.props.noIndent &&
148 this.props.node.comment.parent_id &&
152 <div class="d-flex flex-wrap align-items-center text-muted small">
156 name: node.comment.creator_name,
157 avatar: node.comment.creator_avatar,
158 id: node.comment.creator_id,
159 local: node.comment.creator_local,
160 actor_id: node.comment.creator_actor_id,
165 <div className="badge badge-light d-none d-sm-inline mr-2">
170 <div className="badge badge-light d-none d-sm-inline mr-2">
174 {this.isPostCreator && (
175 <div className="badge badge-light d-none d-sm-inline mr-2">
179 {(node.comment.banned_from_community || node.comment.banned) && (
180 <div className="badge badge-danger mr-2">
184 {this.props.showCommunity && (
186 <span class="mx-1">{i18n.t('to')}</span>
187 <Link class="mr-2" to={`/c/${node.comment.community_name}`}>
188 {node.comment.community_name}
193 className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"
194 onClick={linkEvent(this, this.handleCommentCollapse)}
196 {this.state.collapsed ? (
197 <svg class="icon icon-inline">
198 <use xlinkHref="#icon-plus-square"></use>
201 <svg class="icon icon-inline">
202 <use xlinkHref="#icon-minus-square"></use>
207 className={`unselectable pointer ${this.scoreColor}`}
208 onClick={linkEvent(node, this.handleCommentUpvote)}
209 data-tippy-content={this.pointsTippy}
211 <svg class="icon icon-inline mr-1">
212 <use xlinkHref="#icon-zap"></use>
214 <span class="mr-1">{this.state.score}</span>
216 <span className="mr-1">•</span>
218 <MomentTime data={node.comment} />
221 {/* end of user row */}
222 {this.state.showEdit && (
226 onReplyCancel={this.handleReplyCancel}
227 disabled={this.props.locked}
230 {!this.state.showEdit && !this.state.collapsed && (
232 {this.state.viewSource ? (
233 <pre>{this.commentUnlessRemoved}</pre>
237 dangerouslySetInnerHTML={mdToHtml(
238 this.commentUnlessRemoved
242 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
243 {this.props.showContext && this.linkBtn}
244 {this.props.markable && (
246 class="btn btn-link btn-animate text-muted"
247 onClick={linkEvent(this, this.handleMarkRead)}
250 ? i18n.t('mark_as_unread')
251 : i18n.t('mark_as_read')
254 {this.state.readLoading ? (
258 class={`icon icon-inline ${
259 node.comment.read && 'text-success'
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 {this.props.enableDownvotes && (
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.handleReplyClick)}
304 data-tippy-content={i18n.t('reply')}
306 <svg class="icon icon-inline">
307 <use xlinkHref="#icon-reply1"></use>
310 {!this.state.showAdvanced ? (
312 className="btn btn-link btn-animate text-muted"
313 onClick={linkEvent(this, this.handleShowAdvanced)}
314 data-tippy-content={i18n.t('more')}
316 <svg class="icon icon-inline">
317 <use xlinkHref="#icon-more-vertical"></use>
322 {!this.myComment && (
323 <button class="btn btn-link btn-animate">
326 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
327 title={i18n.t('message').toLowerCase()}
330 <use xlinkHref="#icon-mail"></use>
335 {!this.props.showContext && this.linkBtn}
337 class="btn btn-link btn-animate text-muted"
340 this.handleSaveCommentClick
348 {this.state.saveLoading ? (
352 class={`icon icon-inline ${
353 node.comment.saved && 'text-warning'
356 <use xlinkHref="#icon-star"></use>
361 className="btn btn-link btn-animate text-muted"
362 onClick={linkEvent(this, this.handleViewSource)}
363 data-tippy-content={i18n.t('view_source')}
366 class={`icon icon-inline ${
367 this.state.viewSource && 'text-success'
370 <use xlinkHref="#icon-file-text"></use>
376 class="btn btn-link btn-animate text-muted"
377 onClick={linkEvent(this, this.handleEditClick)}
378 data-tippy-content={i18n.t('edit')}
380 <svg class="icon icon-inline">
381 <use xlinkHref="#icon-edit"></use>
385 class="btn btn-link btn-animate text-muted"
388 this.handleDeleteClick
391 !node.comment.deleted
397 class={`icon icon-inline ${
398 node.comment.deleted && 'text-danger'
401 <use xlinkHref="#icon-trash"></use>
406 {/* Admins and mods can remove comments */}
407 {(this.canMod || this.canAdmin) && (
409 {!node.comment.removed ? (
411 class="btn btn-link btn-animate text-muted"
414 this.handleModRemoveShow
421 class="btn btn-link btn-animate text-muted"
424 this.handleModRemoveSubmit
432 {/* Mods can ban from community, and appoint as mods to community */}
436 (!node.comment.banned_from_community ? (
438 class="btn btn-link btn-animate text-muted"
441 this.handleModBanFromCommunityShow
448 class="btn btn-link btn-animate text-muted"
451 this.handleModBanFromCommunitySubmit
457 {!node.comment.banned_from_community &&
458 (!this.state.showConfirmAppointAsMod ? (
460 class="btn btn-link btn-animate text-muted"
463 this.handleShowConfirmAppointAsMod
467 ? i18n.t('remove_as_mod')
468 : i18n.t('appoint_as_mod')}
472 <button class="btn btn-link btn-animate text-muted">
473 {i18n.t('are_you_sure')}
476 class="btn btn-link btn-animate text-muted"
479 this.handleAddModToCommunity
485 class="btn btn-link btn-animate text-muted"
488 this.handleCancelConfirmAppointAsMod
497 {/* Community creators and admins can transfer community to another mod */}
498 {(this.amCommunityCreator || this.canAdmin) &&
500 (!this.state.showConfirmTransferCommunity ? (
502 class="btn btn-link btn-animate text-muted"
505 this.handleShowConfirmTransferCommunity
508 {i18n.t('transfer_community')}
512 <button class="btn btn-link btn-animate text-muted">
513 {i18n.t('are_you_sure')}
516 class="btn btn-link btn-animate text-muted"
519 this.handleTransferCommunity
525 class="btn btn-link btn-animate text-muted"
529 .handleCancelShowConfirmTransferCommunity
536 {/* Admins can ban from all, and appoint other admins */}
540 (!node.comment.banned ? (
542 class="btn btn-link btn-animate text-muted"
545 this.handleModBanShow
548 {i18n.t('ban_from_site')}
552 class="btn btn-link btn-animate text-muted"
555 this.handleModBanSubmit
558 {i18n.t('unban_from_site')}
561 {!node.comment.banned &&
562 (!this.state.showConfirmAppointAsAdmin ? (
564 class="btn btn-link btn-animate text-muted"
567 this.handleShowConfirmAppointAsAdmin
571 ? i18n.t('remove_as_admin')
572 : i18n.t('appoint_as_admin')}
576 <button class="btn btn-link btn-animate text-muted">
577 {i18n.t('are_you_sure')}
580 class="btn btn-link btn-animate text-muted"
589 class="btn btn-link btn-animate text-muted"
592 this.handleCancelConfirmAppointAsAdmin
601 {/* Site Creator can transfer to another admin */}
602 {this.amSiteCreator &&
604 (!this.state.showConfirmTransferSite ? (
606 class="btn btn-link btn-animate text-muted"
609 this.handleShowConfirmTransferSite
612 {i18n.t('transfer_site')}
616 <button class="btn btn-link btn-animate text-muted">
617 {i18n.t('are_you_sure')}
620 class="btn btn-link btn-animate text-muted"
623 this.handleTransferSite
629 class="btn btn-link btn-animate text-muted"
632 this.handleCancelShowConfirmTransferSite
644 {/* end of button group */}
649 {/* end of details */}
650 {this.state.showRemoveDialog && (
653 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
657 class="form-control mr-2"
658 placeholder={i18n.t('reason')}
659 value={this.state.removeReason}
660 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
662 <button type="submit" class="btn btn-secondary">
663 {i18n.t('remove_comment')}
667 {this.state.showBanDialog && (
668 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
669 <div class="form-group row">
670 <label class="col-form-label">{i18n.t('reason')}</label>
673 class="form-control mr-2"
674 placeholder={i18n.t('reason')}
675 value={this.state.banReason}
676 onInput={linkEvent(this, this.handleModBanReasonChange)}
679 {/* TODO hold off on expires until later */}
680 {/* <div class="form-group row"> */}
681 {/* <label class="col-form-label">Expires</label> */}
682 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
684 <div class="form-group row">
685 <button type="submit" class="btn btn-secondary">
686 {i18n.t('ban')} {node.comment.creator_name}
691 {this.state.showReply && (
694 onReplyCancel={this.handleReplyCancel}
695 disabled={this.props.locked}
698 {node.children && !this.state.collapsed && (
700 nodes={node.children}
701 locked={this.props.locked}
702 moderators={this.props.moderators}
703 admins={this.props.admins}
704 postCreatorId={this.props.postCreatorId}
705 sort={this.props.sort}
706 sortType={this.props.sortType}
707 enableDownvotes={this.props.enableDownvotes}
710 {/* A collapsed clearfix */}
711 {this.state.collapsed && <div class="row col-12"></div>}
717 let node = this.props.node;
720 class="btn btn-link btn-animate text-muted"
721 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
722 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
724 <svg class="icon icon-inline">
725 <use xlinkHref="#icon-link"></use>
733 <svg class="icon icon-spinner spin">
734 <use xlinkHref="#icon-spinner"></use>
739 get myComment(): boolean {
741 UserService.Instance.user &&
742 this.props.node.comment.creator_id == UserService.Instance.user.id
746 get isMod(): boolean {
748 this.props.moderators &&
750 this.props.moderators.map(m => m.user_id),
751 this.props.node.comment.creator_id
756 get isAdmin(): boolean {
760 this.props.admins.map(a => a.id),
761 this.props.node.comment.creator_id
766 get isPostCreator(): boolean {
767 return this.props.node.comment.creator_id == this.props.postCreatorId;
770 get canMod(): boolean {
771 if (this.props.admins && this.props.moderators) {
772 let adminsThenMods = this.props.admins
774 .concat(this.props.moderators.map(m => m.user_id));
777 UserService.Instance.user,
779 this.props.node.comment.creator_id
786 get canAdmin(): boolean {
790 UserService.Instance.user,
791 this.props.admins.map(a => a.id),
792 this.props.node.comment.creator_id
797 get amCommunityCreator(): boolean {
799 this.props.moderators &&
800 UserService.Instance.user &&
801 this.props.node.comment.creator_id != UserService.Instance.user.id &&
802 UserService.Instance.user.id == this.props.moderators[0].user_id
806 get amSiteCreator(): boolean {
809 UserService.Instance.user &&
810 this.props.node.comment.creator_id != UserService.Instance.user.id &&
811 UserService.Instance.user.id == this.props.admins[0].id
815 get commentUnlessRemoved(): string {
816 let node = this.props.node;
817 return node.comment.removed
818 ? `*${i18n.t('removed')}*`
819 : node.comment.deleted
820 ? `*${i18n.t('deleted')}*`
821 : node.comment.content;
824 handleReplyClick(i: CommentNode) {
825 i.state.showReply = true;
829 handleEditClick(i: CommentNode) {
830 i.state.showEdit = true;
834 handleDeleteClick(i: CommentNode) {
835 let deleteForm: CommentFormI = {
836 content: i.props.node.comment.content,
837 edit_id: i.props.node.comment.id,
838 creator_id: i.props.node.comment.creator_id,
839 post_id: i.props.node.comment.post_id,
840 parent_id: i.props.node.comment.parent_id,
841 deleted: !i.props.node.comment.deleted,
844 WebSocketService.Instance.editComment(deleteForm);
847 handleSaveCommentClick(i: CommentNode) {
849 i.props.node.comment.saved == undefined
851 : !i.props.node.comment.saved;
852 let form: SaveCommentForm = {
853 comment_id: i.props.node.comment.id,
857 WebSocketService.Instance.saveComment(form);
859 i.state.saveLoading = true;
860 i.setState(this.state);
863 handleReplyCancel() {
864 this.state.showReply = false;
865 this.state.showEdit = false;
866 this.setState(this.state);
869 handleCommentUpvote(i: CommentNodeI) {
870 let new_vote = this.state.my_vote == 1 ? 0 : 1;
872 if (this.state.my_vote == 1) {
874 this.state.upvotes--;
875 } else if (this.state.my_vote == -1) {
876 this.state.downvotes--;
877 this.state.upvotes++;
878 this.state.score += 2;
880 this.state.upvotes++;
884 this.state.my_vote = new_vote;
886 let form: CommentLikeForm = {
887 comment_id: i.comment.id,
888 post_id: i.comment.post_id,
889 score: this.state.my_vote,
892 WebSocketService.Instance.likeComment(form);
893 this.setState(this.state);
897 handleCommentDownvote(i: CommentNodeI) {
898 let new_vote = this.state.my_vote == -1 ? 0 : -1;
900 if (this.state.my_vote == 1) {
901 this.state.score -= 2;
902 this.state.upvotes--;
903 this.state.downvotes++;
904 } else if (this.state.my_vote == -1) {
905 this.state.downvotes--;
908 this.state.downvotes++;
912 this.state.my_vote = new_vote;
914 let form: CommentLikeForm = {
915 comment_id: i.comment.id,
916 post_id: i.comment.post_id,
917 score: this.state.my_vote,
920 WebSocketService.Instance.likeComment(form);
921 this.setState(this.state);
925 handleModRemoveShow(i: CommentNode) {
926 i.state.showRemoveDialog = true;
930 handleModRemoveReasonChange(i: CommentNode, event: any) {
931 i.state.removeReason = event.target.value;
935 handleModRemoveSubmit(i: CommentNode) {
936 event.preventDefault();
937 let form: CommentFormI = {
938 content: i.props.node.comment.content,
939 edit_id: i.props.node.comment.id,
940 creator_id: i.props.node.comment.creator_id,
941 post_id: i.props.node.comment.post_id,
942 parent_id: i.props.node.comment.parent_id,
943 removed: !i.props.node.comment.removed,
944 reason: i.state.removeReason,
947 WebSocketService.Instance.editComment(form);
949 i.state.showRemoveDialog = false;
953 handleMarkRead(i: CommentNode) {
954 // if it has a user_mention_id field, then its a mention
955 if (i.props.node.comment.user_mention_id) {
956 let form: EditUserMentionForm = {
957 user_mention_id: i.props.node.comment.user_mention_id,
958 read: !i.props.node.comment.read,
960 WebSocketService.Instance.editUserMention(form);
962 let form: CommentFormI = {
963 content: i.props.node.comment.content,
964 edit_id: i.props.node.comment.id,
965 creator_id: i.props.node.comment.creator_id,
966 post_id: i.props.node.comment.post_id,
967 parent_id: i.props.node.comment.parent_id,
968 read: !i.props.node.comment.read,
971 WebSocketService.Instance.editComment(form);
974 i.state.readLoading = true;
975 i.setState(this.state);
978 handleModBanFromCommunityShow(i: CommentNode) {
979 i.state.showBanDialog = !i.state.showBanDialog;
980 i.state.banType = BanType.Community;
984 handleModBanShow(i: CommentNode) {
985 i.state.showBanDialog = !i.state.showBanDialog;
986 i.state.banType = BanType.Site;
990 handleModBanReasonChange(i: CommentNode, event: any) {
991 i.state.banReason = event.target.value;
995 handleModBanExpiresChange(i: CommentNode, event: any) {
996 i.state.banExpires = event.target.value;
1000 handleModBanFromCommunitySubmit(i: CommentNode) {
1001 i.state.banType = BanType.Community;
1002 i.setState(i.state);
1003 i.handleModBanBothSubmit(i);
1006 handleModBanSubmit(i: CommentNode) {
1007 i.state.banType = BanType.Site;
1008 i.setState(i.state);
1009 i.handleModBanBothSubmit(i);
1012 handleModBanBothSubmit(i: CommentNode) {
1013 event.preventDefault();
1015 if (i.state.banType == BanType.Community) {
1016 let form: BanFromCommunityForm = {
1017 user_id: i.props.node.comment.creator_id,
1018 community_id: i.props.node.comment.community_id,
1019 ban: !i.props.node.comment.banned_from_community,
1020 reason: i.state.banReason,
1021 expires: getUnixTime(i.state.banExpires),
1023 WebSocketService.Instance.banFromCommunity(form);
1025 let form: BanUserForm = {
1026 user_id: i.props.node.comment.creator_id,
1027 ban: !i.props.node.comment.banned,
1028 reason: i.state.banReason,
1029 expires: getUnixTime(i.state.banExpires),
1031 WebSocketService.Instance.banUser(form);
1034 i.state.showBanDialog = false;
1035 i.setState(i.state);
1038 handleShowConfirmAppointAsMod(i: CommentNode) {
1039 i.state.showConfirmAppointAsMod = true;
1040 i.setState(i.state);
1043 handleCancelConfirmAppointAsMod(i: CommentNode) {
1044 i.state.showConfirmAppointAsMod = false;
1045 i.setState(i.state);
1048 handleAddModToCommunity(i: CommentNode) {
1049 let form: AddModToCommunityForm = {
1050 user_id: i.props.node.comment.creator_id,
1051 community_id: i.props.node.comment.community_id,
1054 WebSocketService.Instance.addModToCommunity(form);
1055 i.state.showConfirmAppointAsMod = false;
1056 i.setState(i.state);
1059 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1060 i.state.showConfirmAppointAsAdmin = true;
1061 i.setState(i.state);
1064 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1065 i.state.showConfirmAppointAsAdmin = false;
1066 i.setState(i.state);
1069 handleAddAdmin(i: CommentNode) {
1070 let form: AddAdminForm = {
1071 user_id: i.props.node.comment.creator_id,
1074 WebSocketService.Instance.addAdmin(form);
1075 i.state.showConfirmAppointAsAdmin = false;
1076 i.setState(i.state);
1079 handleShowConfirmTransferCommunity(i: CommentNode) {
1080 i.state.showConfirmTransferCommunity = true;
1081 i.setState(i.state);
1084 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1085 i.state.showConfirmTransferCommunity = false;
1086 i.setState(i.state);
1089 handleTransferCommunity(i: CommentNode) {
1090 let form: TransferCommunityForm = {
1091 community_id: i.props.node.comment.community_id,
1092 user_id: i.props.node.comment.creator_id,
1094 WebSocketService.Instance.transferCommunity(form);
1095 i.state.showConfirmTransferCommunity = false;
1096 i.setState(i.state);
1099 handleShowConfirmTransferSite(i: CommentNode) {
1100 i.state.showConfirmTransferSite = true;
1101 i.setState(i.state);
1104 handleCancelShowConfirmTransferSite(i: CommentNode) {
1105 i.state.showConfirmTransferSite = false;
1106 i.setState(i.state);
1109 handleTransferSite(i: CommentNode) {
1110 let form: TransferSiteForm = {
1111 user_id: i.props.node.comment.creator_id,
1113 WebSocketService.Instance.transferSite(form);
1114 i.state.showConfirmTransferSite = false;
1115 i.setState(i.state);
1118 get isCommentNew(): boolean {
1119 let now = moment.utc().subtract(10, 'minutes');
1120 let then = moment.utc(this.props.node.comment.published);
1121 return now.isBefore(then);
1124 handleCommentCollapse(i: CommentNode) {
1125 i.state.collapsed = !i.state.collapsed;
1126 i.setState(i.state);
1129 handleViewSource(i: CommentNode) {
1130 i.state.viewSource = !i.state.viewSource;
1131 i.setState(i.state);
1134 handleShowAdvanced(i: CommentNode) {
1135 i.state.showAdvanced = !i.state.showAdvanced;
1136 i.setState(i.state);
1141 if (this.state.my_vote == 1) {
1143 } else if (this.state.my_vote == -1) {
1144 return 'text-danger';
1146 return 'text-muted';
1150 get pointsTippy(): string {
1151 let points = i18n.t('number_of_points', {
1152 count: this.state.score,
1155 let upvotes = i18n.t('number_of_upvotes', {
1156 count: this.state.upvotes,
1159 let downvotes = i18n.t('number_of_downvotes', {
1160 count: this.state.downvotes,
1163 return `${points} • ${upvotes} • ${downvotes}`;