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 py-2 ${
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 text-muted small">
155 name: node.comment.creator_name,
156 avatar: node.comment.creator_avatar,
161 <div className="badge badge-light d-none d-sm-inline mr-2">
166 <div className="badge badge-light d-none d-sm-inline mr-2">
170 {this.isPostCreator && (
171 <div className="badge badge-light d-none d-sm-inline mr-2">
175 {(node.comment.banned_from_community || node.comment.banned) && (
176 <div className="badge badge-danger mr-2">
180 {this.props.showCommunity && (
182 <span class="mx-1">{i18n.t('to')}</span>
183 <Link class="mr-2" to={`/c/${node.comment.community_name}`}>
184 {node.comment.community_name}
189 className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"
190 onClick={linkEvent(this, this.handleCommentCollapse)}
192 {this.state.collapsed ? (
193 <svg class="icon icon-inline">
194 <use xlinkHref="#icon-plus-square"></use>
197 <svg class="icon icon-inline">
198 <use xlinkHref="#icon-minus-square"></use>
203 className={`unselectable pointer ${this.scoreColor}`}
204 onClick={linkEvent(node, this.handleCommentUpvote)}
205 data-tippy-content={this.pointsTippy}
207 <svg class="icon icon-inline mr-1">
208 <use xlinkHref="#icon-zap"></use>
210 <span class="mr-1">{this.state.score}</span>
212 <span className="mr-1">•</span>
214 <MomentTime data={node.comment} />
217 {/* end of user row */}
218 {this.state.showEdit && (
222 onReplyCancel={this.handleReplyCancel}
223 disabled={this.props.locked}
226 {!this.state.showEdit && !this.state.collapsed && (
228 {this.state.viewSource ? (
229 <pre>{this.commentUnlessRemoved}</pre>
233 dangerouslySetInnerHTML={mdToHtml(
234 this.commentUnlessRemoved
238 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
239 {this.props.showContext && this.linkBtn}
240 {this.props.markable && (
242 class="btn btn-link btn-animate text-muted"
243 onClick={linkEvent(this, this.handleMarkRead)}
246 ? i18n.t('mark_as_unread')
247 : i18n.t('mark_as_read')
250 {this.state.readLoading ? (
254 class={`icon icon-inline ${
255 node.comment.read && 'text-success'
258 <use xlinkHref="#icon-check"></use>
263 {UserService.Instance.user && !this.props.viewOnly && (
266 className={`btn btn-link btn-animate ${
267 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
269 onClick={linkEvent(node, this.handleCommentUpvote)}
270 data-tippy-content={i18n.t('upvote')}
272 <svg class="icon icon-inline">
273 <use xlinkHref="#icon-arrow-up"></use>
275 {this.state.upvotes !== this.state.score && (
276 <span class="ml-1">{this.state.upvotes}</span>
279 {WebSocketService.Instance.site.enable_downvotes && (
281 className={`btn btn-link btn-animate ${
282 this.state.my_vote == -1
286 onClick={linkEvent(node, this.handleCommentDownvote)}
287 data-tippy-content={i18n.t('downvote')}
289 <svg class="icon icon-inline">
290 <use xlinkHref="#icon-arrow-down"></use>
292 {this.state.upvotes !== this.state.score && (
293 <span class="ml-1">{this.state.downvotes}</span>
298 class="btn btn-link btn-animate text-muted"
299 onClick={linkEvent(this, this.handleReplyClick)}
300 data-tippy-content={i18n.t('reply')}
302 <svg class="icon icon-inline">
303 <use xlinkHref="#icon-reply1"></use>
306 {!this.state.showAdvanced ? (
308 className="btn btn-link btn-animate text-muted"
309 onClick={linkEvent(this, this.handleShowAdvanced)}
310 data-tippy-content={i18n.t('more')}
312 <svg class="icon icon-inline">
313 <use xlinkHref="#icon-more-vertical"></use>
318 {!this.myComment && (
319 <button class="btn btn-link btn-animate">
322 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
323 title={i18n.t('message').toLowerCase()}
326 <use xlinkHref="#icon-mail"></use>
331 {!this.props.showContext && this.linkBtn}
333 class="btn btn-link btn-animate text-muted"
336 this.handleSaveCommentClick
344 {this.state.saveLoading ? (
348 class={`icon icon-inline ${
349 node.comment.saved && 'text-warning'
352 <use xlinkHref="#icon-star"></use>
357 className="btn btn-link btn-animate text-muted"
358 onClick={linkEvent(this, this.handleViewSource)}
359 data-tippy-content={i18n.t('view_source')}
362 class={`icon icon-inline ${
363 this.state.viewSource && 'text-success'
366 <use xlinkHref="#icon-file-text"></use>
372 class="btn btn-link btn-animate text-muted"
373 onClick={linkEvent(this, this.handleEditClick)}
374 data-tippy-content={i18n.t('edit')}
376 <svg class="icon icon-inline">
377 <use xlinkHref="#icon-edit"></use>
381 class="btn btn-link btn-animate text-muted"
384 this.handleDeleteClick
387 !node.comment.deleted
393 class={`icon icon-inline ${
394 node.comment.deleted && 'text-danger'
397 <use xlinkHref="#icon-trash"></use>
402 {/* Admins and mods can remove comments */}
403 {(this.canMod || this.canAdmin) && (
405 {!node.comment.removed ? (
407 class="btn btn-link btn-animate text-muted"
410 this.handleModRemoveShow
417 class="btn btn-link btn-animate text-muted"
420 this.handleModRemoveSubmit
428 {/* Mods can ban from community, and appoint as mods to community */}
432 (!node.comment.banned_from_community ? (
434 class="btn btn-link btn-animate text-muted"
437 this.handleModBanFromCommunityShow
444 class="btn btn-link btn-animate text-muted"
447 this.handleModBanFromCommunitySubmit
453 {!node.comment.banned_from_community &&
454 (!this.state.showConfirmAppointAsMod ? (
456 class="btn btn-link btn-animate text-muted"
459 this.handleShowConfirmAppointAsMod
463 ? i18n.t('remove_as_mod')
464 : i18n.t('appoint_as_mod')}
468 <button class="btn btn-link btn-animate text-muted">
469 {i18n.t('are_you_sure')}
472 class="btn btn-link btn-animate text-muted"
475 this.handleAddModToCommunity
481 class="btn btn-link btn-animate text-muted"
484 this.handleCancelConfirmAppointAsMod
493 {/* Community creators and admins can transfer community to another mod */}
494 {(this.amCommunityCreator || this.canAdmin) &&
496 (!this.state.showConfirmTransferCommunity ? (
498 class="btn btn-link btn-animate text-muted"
501 this.handleShowConfirmTransferCommunity
504 {i18n.t('transfer_community')}
508 <button class="btn btn-link btn-animate text-muted">
509 {i18n.t('are_you_sure')}
512 class="btn btn-link btn-animate text-muted"
515 this.handleTransferCommunity
521 class="btn btn-link btn-animate text-muted"
525 .handleCancelShowConfirmTransferCommunity
532 {/* Admins can ban from all, and appoint other admins */}
536 (!node.comment.banned ? (
538 class="btn btn-link btn-animate text-muted"
541 this.handleModBanShow
544 {i18n.t('ban_from_site')}
548 class="btn btn-link btn-animate text-muted"
551 this.handleModBanSubmit
554 {i18n.t('unban_from_site')}
557 {!node.comment.banned &&
558 (!this.state.showConfirmAppointAsAdmin ? (
560 class="btn btn-link btn-animate text-muted"
563 this.handleShowConfirmAppointAsAdmin
567 ? i18n.t('remove_as_admin')
568 : i18n.t('appoint_as_admin')}
572 <button class="btn btn-link btn-animate text-muted">
573 {i18n.t('are_you_sure')}
576 class="btn btn-link btn-animate text-muted"
585 class="btn btn-link btn-animate text-muted"
588 this.handleCancelConfirmAppointAsAdmin
597 {/* Site Creator can transfer to another admin */}
598 {this.amSiteCreator &&
600 (!this.state.showConfirmTransferSite ? (
602 class="btn btn-link btn-animate text-muted"
605 this.handleShowConfirmTransferSite
608 {i18n.t('transfer_site')}
612 <button class="btn btn-link btn-animate text-muted">
613 {i18n.t('are_you_sure')}
616 class="btn btn-link btn-animate text-muted"
619 this.handleTransferSite
625 class="btn btn-link btn-animate text-muted"
628 this.handleCancelShowConfirmTransferSite
640 {/* end of button group */}
645 {/* end of details */}
646 {this.state.showRemoveDialog && (
649 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
653 class="form-control mr-2"
654 placeholder={i18n.t('reason')}
655 value={this.state.removeReason}
656 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
658 <button type="submit" class="btn btn-secondary">
659 {i18n.t('remove_comment')}
663 {this.state.showBanDialog && (
664 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
665 <div class="form-group row">
666 <label class="col-form-label">{i18n.t('reason')}</label>
669 class="form-control mr-2"
670 placeholder={i18n.t('reason')}
671 value={this.state.banReason}
672 onInput={linkEvent(this, this.handleModBanReasonChange)}
675 {/* TODO hold off on expires until later */}
676 {/* <div class="form-group row"> */}
677 {/* <label class="col-form-label">Expires</label> */}
678 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
680 <div class="form-group row">
681 <button type="submit" class="btn btn-secondary">
682 {i18n.t('ban')} {node.comment.creator_name}
687 {this.state.showReply && (
690 onReplyCancel={this.handleReplyCancel}
691 disabled={this.props.locked}
694 {node.children && !this.state.collapsed && (
696 nodes={node.children}
697 locked={this.props.locked}
698 moderators={this.props.moderators}
699 admins={this.props.admins}
700 postCreatorId={this.props.postCreatorId}
701 sort={this.props.sort}
702 sortType={this.props.sortType}
705 {/* A collapsed clearfix */}
706 {this.state.collapsed && <div class="row col-12"></div>}
712 let node = this.props.node;
715 class="btn btn-link btn-animate text-muted"
716 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
717 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
719 <svg class="icon icon-inline">
720 <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}`;