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;
78 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
79 private emptyState: CommentNodeState = {
82 showRemoveDialog: false,
87 banType: BanType.Community,
91 showConfirmTransferSite: false,
92 showConfirmTransferCommunity: false,
93 showConfirmAppointAsMod: false,
94 showConfirmAppointAsAdmin: false,
95 my_vote: this.props.node.comment.my_vote,
96 score: this.props.node.comment.score,
97 upvotes: this.props.node.comment.upvotes,
98 downvotes: this.props.node.comment.downvotes,
99 borderColor: this.props.node.comment.depth
100 ? colorList[this.props.node.comment.depth % colorList.length]
106 constructor(props: any, context: any) {
107 super(props, context);
109 this.state = this.emptyState;
110 this.handleReplyCancel = this.handleReplyCancel.bind(this);
111 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
112 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
115 componentWillReceiveProps(nextProps: CommentNodeProps) {
116 this.state.my_vote = nextProps.node.comment.my_vote;
117 this.state.upvotes = nextProps.node.comment.upvotes;
118 this.state.downvotes = nextProps.node.comment.downvotes;
119 this.state.score = nextProps.node.comment.score;
120 this.state.readLoading = false;
121 this.state.saveLoading = false;
122 this.setState(this.state);
126 let node = this.props.node;
129 className={`comment ${
130 node.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
134 id={`comment-${node.comment.id}`}
135 className={`details comment-node border-top border-light ${
136 this.isCommentNew ? 'mark' : ''
139 !this.props.noIndent &&
140 this.props.node.comment.parent_id &&
141 `border-left: 2px ${this.state.borderColor} solid !important`
146 !this.props.noIndent &&
147 this.props.node.comment.parent_id &&
151 <div class="d-flex flex-wrap align-items-center mb-1 mt-1 text-muted small">
155 name: node.comment.creator_name,
156 avatar: node.comment.creator_avatar,
157 id: node.comment.creator_id,
158 local: node.comment.creator_local,
159 actor_id: node.comment.creator_actor_id,
164 <div className="badge badge-light d-none d-sm-inline mr-2">
169 <div className="badge badge-light d-none d-sm-inline mr-2">
173 {this.isPostCreator && (
174 <div className="badge badge-light d-none d-sm-inline mr-2">
178 {(node.comment.banned_from_community || node.comment.banned) && (
179 <div className="badge badge-danger mr-2">
183 {this.props.showCommunity && (
185 <span class="mx-1">{i18n.t('to')}</span>
186 <Link class="mr-2" to={`/c/${node.comment.community_name}`}>
187 {node.comment.community_name}
192 className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"
193 onClick={linkEvent(this, this.handleCommentCollapse)}
195 {this.state.collapsed ? (
196 <svg class="icon icon-inline">
197 <use xlinkHref="#icon-plus-square"></use>
200 <svg class="icon icon-inline">
201 <use xlinkHref="#icon-minus-square"></use>
206 className={`unselectable pointer ${this.scoreColor}`}
207 onClick={linkEvent(node, this.handleCommentUpvote)}
208 data-tippy-content={this.pointsTippy}
210 <svg class="icon icon-inline mr-1">
211 <use xlinkHref="#icon-zap"></use>
213 <span class="mr-1">{this.state.score}</span>
215 <span className="mr-1">•</span>
217 <MomentTime data={node.comment} />
220 {/* end of user row */}
221 {this.state.showEdit && (
225 onReplyCancel={this.handleReplyCancel}
226 disabled={this.props.locked}
229 {!this.state.showEdit && !this.state.collapsed && (
231 {this.state.viewSource ? (
232 <pre>{this.commentUnlessRemoved}</pre>
236 dangerouslySetInnerHTML={mdToHtml(
237 this.commentUnlessRemoved
241 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
242 {this.props.showContext && this.linkBtn}
243 {this.props.markable && (
245 class="btn btn-link btn-animate text-muted"
246 onClick={linkEvent(this, this.handleMarkRead)}
249 ? i18n.t('mark_as_unread')
250 : i18n.t('mark_as_read')
253 {this.state.readLoading ? (
257 class={`icon icon-inline ${
258 node.comment.read && 'text-success'
261 <use xlinkHref="#icon-check"></use>
266 {UserService.Instance.user && !this.props.viewOnly && (
269 className={`btn btn-link btn-animate ${
270 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
272 onClick={linkEvent(node, this.handleCommentUpvote)}
273 data-tippy-content={i18n.t('upvote')}
275 <svg class="icon icon-inline">
276 <use xlinkHref="#icon-arrow-up"></use>
278 {this.state.upvotes !== this.state.score && (
279 <span class="ml-1">{this.state.upvotes}</span>
282 {WebSocketService.Instance.site.enable_downvotes && (
284 className={`btn btn-link btn-animate ${
285 this.state.my_vote == -1
289 onClick={linkEvent(node, this.handleCommentDownvote)}
290 data-tippy-content={i18n.t('downvote')}
292 <svg class="icon icon-inline">
293 <use xlinkHref="#icon-arrow-down"></use>
295 {this.state.upvotes !== this.state.score && (
296 <span class="ml-1">{this.state.downvotes}</span>
301 class="btn btn-link btn-animate text-muted"
302 onClick={linkEvent(this, this.handleSaveCommentClick)}
304 node.comment.saved ? i18n.t('unsave') : i18n.t('save')
307 {this.state.saveLoading ? (
311 class={`icon icon-inline ${
312 node.comment.saved && 'text-warning'
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 ${
361 this.state.viewSource && 'text-success'
364 <use xlinkHref="#icon-file-text"></use>
370 class="btn btn-link btn-animate text-muted"
371 onClick={linkEvent(this, this.handleEditClick)}
372 data-tippy-content={i18n.t('edit')}
374 <svg class="icon icon-inline">
375 <use xlinkHref="#icon-edit"></use>
379 class="btn btn-link btn-animate text-muted"
382 this.handleDeleteClick
385 !node.comment.deleted
391 class={`icon icon-inline ${
392 node.comment.deleted && 'text-danger'
395 <use xlinkHref="#icon-trash"></use>
400 {/* Admins and mods can remove comments */}
401 {(this.canMod || this.canAdmin) && (
403 {!node.comment.removed ? (
405 class="btn btn-link btn-animate text-muted"
408 this.handleModRemoveShow
415 class="btn btn-link btn-animate text-muted"
418 this.handleModRemoveSubmit
426 {/* Mods can ban from community, and appoint as mods to community */}
430 (!node.comment.banned_from_community ? (
432 class="btn btn-link btn-animate text-muted"
435 this.handleModBanFromCommunityShow
442 class="btn btn-link btn-animate text-muted"
445 this.handleModBanFromCommunitySubmit
451 {!node.comment.banned_from_community &&
452 (!this.state.showConfirmAppointAsMod ? (
454 class="btn btn-link btn-animate text-muted"
457 this.handleShowConfirmAppointAsMod
461 ? i18n.t('remove_as_mod')
462 : i18n.t('appoint_as_mod')}
466 <button class="btn btn-link btn-animate text-muted">
467 {i18n.t('are_you_sure')}
470 class="btn btn-link btn-animate text-muted"
473 this.handleAddModToCommunity
479 class="btn btn-link btn-animate text-muted"
482 this.handleCancelConfirmAppointAsMod
491 {/* Community creators and admins can transfer community to another mod */}
492 {(this.amCommunityCreator || this.canAdmin) &&
494 (!this.state.showConfirmTransferCommunity ? (
496 class="btn btn-link btn-animate text-muted"
499 this.handleShowConfirmTransferCommunity
502 {i18n.t('transfer_community')}
506 <button class="btn btn-link btn-animate text-muted">
507 {i18n.t('are_you_sure')}
510 class="btn btn-link btn-animate text-muted"
513 this.handleTransferCommunity
519 class="btn btn-link btn-animate text-muted"
523 .handleCancelShowConfirmTransferCommunity
530 {/* Admins can ban from all, and appoint other admins */}
534 (!node.comment.banned ? (
536 class="btn btn-link btn-animate text-muted"
539 this.handleModBanShow
542 {i18n.t('ban_from_site')}
546 class="btn btn-link btn-animate text-muted"
549 this.handleModBanSubmit
552 {i18n.t('unban_from_site')}
555 {!node.comment.banned &&
556 (!this.state.showConfirmAppointAsAdmin ? (
558 class="btn btn-link btn-animate text-muted"
561 this.handleShowConfirmAppointAsAdmin
565 ? i18n.t('remove_as_admin')
566 : i18n.t('appoint_as_admin')}
570 <button class="btn btn-link btn-animate text-muted">
571 {i18n.t('are_you_sure')}
574 class="btn btn-link btn-animate text-muted"
583 class="btn btn-link btn-animate text-muted"
586 this.handleCancelConfirmAppointAsAdmin
595 {/* Site Creator can transfer to another admin */}
596 {this.amSiteCreator &&
598 (!this.state.showConfirmTransferSite ? (
600 class="btn btn-link btn-animate text-muted"
603 this.handleShowConfirmTransferSite
606 {i18n.t('transfer_site')}
610 <button class="btn btn-link btn-animate text-muted">
611 {i18n.t('are_you_sure')}
614 class="btn btn-link btn-animate text-muted"
617 this.handleTransferSite
623 class="btn btn-link btn-animate text-muted"
626 this.handleCancelShowConfirmTransferSite
638 {/* end of button group */}
643 {/* end of details */}
644 {this.state.showRemoveDialog && (
647 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
651 class="form-control mr-2"
652 placeholder={i18n.t('reason')}
653 value={this.state.removeReason}
654 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
656 <button type="submit" class="btn btn-secondary">
657 {i18n.t('remove_comment')}
661 {this.state.showBanDialog && (
662 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
663 <div class="form-group row">
664 <label class="col-form-label">{i18n.t('reason')}</label>
667 class="form-control mr-2"
668 placeholder={i18n.t('reason')}
669 value={this.state.banReason}
670 onInput={linkEvent(this, this.handleModBanReasonChange)}
673 {/* TODO hold off on expires until later */}
674 {/* <div class="form-group row"> */}
675 {/* <label class="col-form-label">Expires</label> */}
676 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
678 <div class="form-group row">
679 <button type="submit" class="btn btn-secondary">
680 {i18n.t('ban')} {node.comment.creator_name}
685 {this.state.showReply && (
688 onReplyCancel={this.handleReplyCancel}
689 disabled={this.props.locked}
692 {node.children && !this.state.collapsed && (
694 nodes={node.children}
695 locked={this.props.locked}
696 moderators={this.props.moderators}
697 admins={this.props.admins}
698 postCreatorId={this.props.postCreatorId}
699 sort={this.props.sort}
700 sortType={this.props.sortType}
703 {/* A collapsed clearfix */}
704 {this.state.collapsed && <div class="row col-12"></div>}
710 let node = this.props.node;
713 class="btn btn-link btn-animate text-muted"
714 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
715 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
717 <svg class="icon icon-inline">
718 <use xlinkHref="#icon-link"></use>
726 <svg class="icon icon-spinner spin">
727 <use xlinkHref="#icon-spinner"></use>
732 get myComment(): boolean {
734 UserService.Instance.user &&
735 this.props.node.comment.creator_id == UserService.Instance.user.id
739 get isMod(): boolean {
741 this.props.moderators &&
743 this.props.moderators.map(m => m.user_id),
744 this.props.node.comment.creator_id
749 get isAdmin(): boolean {
753 this.props.admins.map(a => a.id),
754 this.props.node.comment.creator_id
759 get isPostCreator(): boolean {
760 return this.props.node.comment.creator_id == this.props.postCreatorId;
763 get canMod(): boolean {
764 if (this.props.admins && this.props.moderators) {
765 let adminsThenMods = this.props.admins
767 .concat(this.props.moderators.map(m => m.user_id));
770 UserService.Instance.user,
772 this.props.node.comment.creator_id
779 get canAdmin(): boolean {
783 UserService.Instance.user,
784 this.props.admins.map(a => a.id),
785 this.props.node.comment.creator_id
790 get amCommunityCreator(): boolean {
792 this.props.moderators &&
793 UserService.Instance.user &&
794 this.props.node.comment.creator_id != UserService.Instance.user.id &&
795 UserService.Instance.user.id == this.props.moderators[0].user_id
799 get amSiteCreator(): boolean {
802 UserService.Instance.user &&
803 this.props.node.comment.creator_id != UserService.Instance.user.id &&
804 UserService.Instance.user.id == this.props.admins[0].id
808 get commentUnlessRemoved(): string {
809 let node = this.props.node;
810 return node.comment.removed
811 ? `*${i18n.t('removed')}*`
812 : node.comment.deleted
813 ? `*${i18n.t('deleted')}*`
814 : node.comment.content;
817 handleReplyClick(i: CommentNode) {
818 i.state.showReply = true;
822 handleEditClick(i: CommentNode) {
823 i.state.showEdit = true;
827 handleDeleteClick(i: CommentNode) {
828 let deleteForm: CommentFormI = {
829 content: i.props.node.comment.content,
830 edit_id: i.props.node.comment.id,
831 creator_id: i.props.node.comment.creator_id,
832 post_id: i.props.node.comment.post_id,
833 parent_id: i.props.node.comment.parent_id,
834 deleted: !i.props.node.comment.deleted,
837 WebSocketService.Instance.editComment(deleteForm);
840 handleSaveCommentClick(i: CommentNode) {
842 i.props.node.comment.saved == undefined
844 : !i.props.node.comment.saved;
845 let form: SaveCommentForm = {
846 comment_id: i.props.node.comment.id,
850 WebSocketService.Instance.saveComment(form);
852 i.state.saveLoading = true;
853 i.setState(this.state);
856 handleReplyCancel() {
857 this.state.showReply = false;
858 this.state.showEdit = false;
859 this.setState(this.state);
862 handleCommentUpvote(i: CommentNodeI) {
863 let new_vote = this.state.my_vote == 1 ? 0 : 1;
865 if (this.state.my_vote == 1) {
867 this.state.upvotes--;
868 } else if (this.state.my_vote == -1) {
869 this.state.downvotes--;
870 this.state.upvotes++;
871 this.state.score += 2;
873 this.state.upvotes++;
877 this.state.my_vote = new_vote;
879 let form: CommentLikeForm = {
880 comment_id: i.comment.id,
881 post_id: i.comment.post_id,
882 score: this.state.my_vote,
885 WebSocketService.Instance.likeComment(form);
886 this.setState(this.state);
890 handleCommentDownvote(i: CommentNodeI) {
891 let new_vote = this.state.my_vote == -1 ? 0 : -1;
893 if (this.state.my_vote == 1) {
894 this.state.score -= 2;
895 this.state.upvotes--;
896 this.state.downvotes++;
897 } else if (this.state.my_vote == -1) {
898 this.state.downvotes--;
901 this.state.downvotes++;
905 this.state.my_vote = new_vote;
907 let form: CommentLikeForm = {
908 comment_id: i.comment.id,
909 post_id: i.comment.post_id,
910 score: this.state.my_vote,
913 WebSocketService.Instance.likeComment(form);
914 this.setState(this.state);
918 handleModRemoveShow(i: CommentNode) {
919 i.state.showRemoveDialog = true;
923 handleModRemoveReasonChange(i: CommentNode, event: any) {
924 i.state.removeReason = event.target.value;
928 handleModRemoveSubmit(i: CommentNode) {
929 event.preventDefault();
930 let form: CommentFormI = {
931 content: i.props.node.comment.content,
932 edit_id: i.props.node.comment.id,
933 creator_id: i.props.node.comment.creator_id,
934 post_id: i.props.node.comment.post_id,
935 parent_id: i.props.node.comment.parent_id,
936 removed: !i.props.node.comment.removed,
937 reason: i.state.removeReason,
940 WebSocketService.Instance.editComment(form);
942 i.state.showRemoveDialog = false;
946 handleMarkRead(i: CommentNode) {
947 // if it has a user_mention_id field, then its a mention
948 if (i.props.node.comment.user_mention_id) {
949 let form: EditUserMentionForm = {
950 user_mention_id: i.props.node.comment.user_mention_id,
951 read: !i.props.node.comment.read,
953 WebSocketService.Instance.editUserMention(form);
955 let form: CommentFormI = {
956 content: i.props.node.comment.content,
957 edit_id: i.props.node.comment.id,
958 creator_id: i.props.node.comment.creator_id,
959 post_id: i.props.node.comment.post_id,
960 parent_id: i.props.node.comment.parent_id,
961 read: !i.props.node.comment.read,
964 WebSocketService.Instance.editComment(form);
967 i.state.readLoading = true;
968 i.setState(this.state);
971 handleModBanFromCommunityShow(i: CommentNode) {
972 i.state.showBanDialog = !i.state.showBanDialog;
973 i.state.banType = BanType.Community;
977 handleModBanShow(i: CommentNode) {
978 i.state.showBanDialog = !i.state.showBanDialog;
979 i.state.banType = BanType.Site;
983 handleModBanReasonChange(i: CommentNode, event: any) {
984 i.state.banReason = event.target.value;
988 handleModBanExpiresChange(i: CommentNode, event: any) {
989 i.state.banExpires = event.target.value;
993 handleModBanFromCommunitySubmit(i: CommentNode) {
994 i.state.banType = BanType.Community;
996 i.handleModBanBothSubmit(i);
999 handleModBanSubmit(i: CommentNode) {
1000 i.state.banType = BanType.Site;
1001 i.setState(i.state);
1002 i.handleModBanBothSubmit(i);
1005 handleModBanBothSubmit(i: CommentNode) {
1006 event.preventDefault();
1008 if (i.state.banType == BanType.Community) {
1009 let form: BanFromCommunityForm = {
1010 user_id: i.props.node.comment.creator_id,
1011 community_id: i.props.node.comment.community_id,
1012 ban: !i.props.node.comment.banned_from_community,
1013 reason: i.state.banReason,
1014 expires: getUnixTime(i.state.banExpires),
1016 WebSocketService.Instance.banFromCommunity(form);
1018 let form: BanUserForm = {
1019 user_id: i.props.node.comment.creator_id,
1020 ban: !i.props.node.comment.banned,
1021 reason: i.state.banReason,
1022 expires: getUnixTime(i.state.banExpires),
1024 WebSocketService.Instance.banUser(form);
1027 i.state.showBanDialog = false;
1028 i.setState(i.state);
1031 handleShowConfirmAppointAsMod(i: CommentNode) {
1032 i.state.showConfirmAppointAsMod = true;
1033 i.setState(i.state);
1036 handleCancelConfirmAppointAsMod(i: CommentNode) {
1037 i.state.showConfirmAppointAsMod = false;
1038 i.setState(i.state);
1041 handleAddModToCommunity(i: CommentNode) {
1042 let form: AddModToCommunityForm = {
1043 user_id: i.props.node.comment.creator_id,
1044 community_id: i.props.node.comment.community_id,
1047 WebSocketService.Instance.addModToCommunity(form);
1048 i.state.showConfirmAppointAsMod = false;
1049 i.setState(i.state);
1052 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1053 i.state.showConfirmAppointAsAdmin = true;
1054 i.setState(i.state);
1057 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1058 i.state.showConfirmAppointAsAdmin = false;
1059 i.setState(i.state);
1062 handleAddAdmin(i: CommentNode) {
1063 let form: AddAdminForm = {
1064 user_id: i.props.node.comment.creator_id,
1067 WebSocketService.Instance.addAdmin(form);
1068 i.state.showConfirmAppointAsAdmin = false;
1069 i.setState(i.state);
1072 handleShowConfirmTransferCommunity(i: CommentNode) {
1073 i.state.showConfirmTransferCommunity = true;
1074 i.setState(i.state);
1077 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1078 i.state.showConfirmTransferCommunity = false;
1079 i.setState(i.state);
1082 handleTransferCommunity(i: CommentNode) {
1083 let form: TransferCommunityForm = {
1084 community_id: i.props.node.comment.community_id,
1085 user_id: i.props.node.comment.creator_id,
1087 WebSocketService.Instance.transferCommunity(form);
1088 i.state.showConfirmTransferCommunity = false;
1089 i.setState(i.state);
1092 handleShowConfirmTransferSite(i: CommentNode) {
1093 i.state.showConfirmTransferSite = true;
1094 i.setState(i.state);
1097 handleCancelShowConfirmTransferSite(i: CommentNode) {
1098 i.state.showConfirmTransferSite = false;
1099 i.setState(i.state);
1102 handleTransferSite(i: CommentNode) {
1103 let form: TransferSiteForm = {
1104 user_id: i.props.node.comment.creator_id,
1106 WebSocketService.Instance.transferSite(form);
1107 i.state.showConfirmTransferSite = false;
1108 i.setState(i.state);
1111 get isCommentNew(): boolean {
1112 let now = moment.utc().subtract(10, 'minutes');
1113 let then = moment.utc(this.props.node.comment.published);
1114 return now.isBefore(then);
1117 handleCommentCollapse(i: CommentNode) {
1118 i.state.collapsed = !i.state.collapsed;
1119 i.setState(i.state);
1122 handleViewSource(i: CommentNode) {
1123 i.state.viewSource = !i.state.viewSource;
1124 i.setState(i.state);
1127 handleShowAdvanced(i: CommentNode) {
1128 i.state.showAdvanced = !i.state.showAdvanced;
1129 i.setState(i.state);
1134 if (this.state.my_vote == 1) {
1136 } else if (this.state.my_vote == -1) {
1137 return 'text-danger';
1139 return 'text-muted';
1143 get pointsTippy(): string {
1144 let points = i18n.t('number_of_points', {
1145 count: this.state.score,
1148 let upvotes = i18n.t('number_of_upvotes', {
1149 count: this.state.upvotes,
1152 let downvotes = i18n.t('number_of_downvotes', {
1153 count: this.state.downvotes,
1156 return `${points} • ${upvotes} • ${downvotes}`;