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 <ul class="list-inline mb-1 text-muted small">
151 <li className="mt-1 list-inline-item">
153 className="text-body font-weight-bold"
154 to={`/u/${node.comment.creator_name}`}
156 {node.comment.creator_avatar && showAvatars() && (
160 src={pictshareAvatarThumbnail(
161 node.comment.creator_avatar
163 class="rounded-circle mr-1"
166 <span>{node.comment.creator_name}</span>
170 <li className="list-inline-item badge badge-light d-none d-sm-inline">
175 <li className="list-inline-item badge badge-light d-none d-sm-inline">
179 {this.isPostCreator && (
180 <li className="list-inline-item badge badge-light d-none d-sm-inline">
184 {(node.comment.banned_from_community || node.comment.banned) && (
185 <li className="list-inline-item badge badge-danger">
189 <li className="list-inline-item">
191 className="unselectable pointer text-monospace"
192 onClick={linkEvent(this, this.handleCommentCollapse)}
194 {this.state.collapsed ? (
195 <svg class="icon icon-inline">
196 <use xlinkHref="#icon-plus-square"></use>
199 <svg class="icon icon-inline">
200 <use xlinkHref="#icon-minus-square"></use>
205 {this.props.showCommunity && (
206 <li className="list-inline-item">
207 <span> {i18n.t('to')} </span>
208 <Link to={`/c/${node.comment.community_name}`}>
209 {node.comment.community_name}
213 <li className="ml-3 list-inline-item">
215 className={`unselectable pointer ${this.scoreColor}`}
216 onClick={linkEvent(node, this.handleCommentUpvote)}
217 data-tippy-content={this.pointsTippy}
219 <svg class="icon icon-inline mr-1">
220 <use xlinkHref="#icon-zap"></use>
225 <li className="list-inline-item">•</li>
226 <li className="list-inline-item">
228 <MomentTime data={node.comment} />
232 {this.state.showEdit && (
236 onReplyCancel={this.handleReplyCancel}
237 disabled={this.props.locked}
240 {!this.state.showEdit && !this.state.collapsed && (
242 {this.state.viewSource ? (
243 <pre>{this.commentUnlessRemoved}</pre>
247 dangerouslySetInnerHTML={mdToHtml(
248 this.commentUnlessRemoved
252 <ul class="list-inline mb-0 text-muted font-weight-bold small">
253 {this.props.showContext && this.linkBtn}
254 {this.props.markable && (
255 <li className="list-inline-item">
257 class="btn btn-link btn-animate text-muted"
258 onClick={linkEvent(this, this.handleMarkRead)}
261 ? i18n.t('mark_as_unread')
262 : i18n.t('mark_as_read')
265 {this.state.readLoading ? (
269 class={`icon icon-inline ${node.comment.read &&
272 <use xlinkHref="#icon-check"></use>
278 {UserService.Instance.user && !this.props.viewOnly && (
280 <li className="list-inline-item">
282 className={`btn btn-link btn-animate ${
283 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
285 onClick={linkEvent(node, this.handleCommentUpvote)}
286 data-tippy-content={i18n.t('upvote')}
288 <svg class="icon icon-inline">
289 <use xlinkHref="#icon-arrow-up"></use>
291 {this.state.upvotes !== this.state.score && (
292 <span class="ml-1">{this.state.upvotes}</span>
296 {WebSocketService.Instance.site.enable_downvotes && (
297 <li className="list-inline-item">
299 className={`btn btn-link btn-animate ${
300 this.state.my_vote == -1
306 this.handleCommentDownvote
308 data-tippy-content={i18n.t('downvote')}
310 <svg class="icon icon-inline">
311 <use xlinkHref="#icon-arrow-down"></use>
313 {this.state.upvotes !== this.state.score && (
314 <span class="ml-1">{this.state.downvotes}</span>
319 <li className="list-inline-item">
321 class="btn btn-link btn-animate text-muted"
322 onClick={linkEvent(this, this.handleSaveCommentClick)}
329 {this.state.saveLoading ? (
333 class={`icon icon-inline ${node.comment.saved &&
336 <use xlinkHref="#icon-star"></use>
341 <li className="list-inline-item">
343 class="btn btn-link btn-animate text-muted"
344 onClick={linkEvent(this, this.handleReplyClick)}
345 data-tippy-content={i18n.t('reply')}
347 <svg class="icon icon-inline">
348 <use xlinkHref="#icon-reply1"></use>
352 {!this.state.showAdvanced ? (
353 <li className="list-inline-item">
355 className="btn btn-link btn-animate text-muted"
356 onClick={linkEvent(this, this.handleShowAdvanced)}
357 data-tippy-content={i18n.t('more')}
359 <svg class="icon icon-inline">
360 <use xlinkHref="#icon-more-vertical"></use>
366 {!this.myComment && (
367 <li className="list-inline-item">
369 class="btn btn-link btn-animate text-muted"
370 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
371 title={i18n.t('message').toLowerCase()}
374 <use xlinkHref="#icon-mail"></use>
379 {!this.props.showContext && this.linkBtn}
380 <li className="list-inline-item">
382 className="btn btn-link btn-animate text-muted"
383 onClick={linkEvent(this, this.handleViewSource)}
384 data-tippy-content={i18n.t('view_source')}
387 class={`icon icon-inline ${this.state
388 .viewSource && 'text-success'}`}
390 <use xlinkHref="#icon-file-text"></use>
396 <li className="list-inline-item">•</li>
397 <li className="list-inline-item">
399 class="btn btn-link btn-animate text-muted"
404 data-tippy-content={i18n.t('edit')}
406 <svg class="icon icon-inline">
407 <use xlinkHref="#icon-edit"></use>
411 <li className="list-inline-item">
413 class="btn btn-link btn-animate text-muted"
416 this.handleDeleteClick
419 !node.comment.deleted
425 class={`icon icon-inline ${node.comment
426 .deleted && 'text-danger'}`}
428 <use xlinkHref="#icon-trash"></use>
434 {/* Admins and mods can remove comments */}
435 {(this.canMod || this.canAdmin) && (
437 <li className="list-inline-item">
438 {!node.comment.removed ? (
443 this.handleModRemoveShow
453 this.handleModRemoveSubmit
462 {/* Mods can ban from community, and appoint as mods to community */}
466 <li className="list-inline-item">
467 {!node.comment.banned_from_community ? (
472 this.handleModBanFromCommunityShow
482 this.handleModBanFromCommunitySubmit
490 {!node.comment.banned_from_community && (
491 <li className="list-inline-item">
492 {!this.state.showConfirmAppointAsMod ? (
497 this.handleShowConfirmAppointAsMod
501 ? i18n.t('remove_as_mod')
502 : i18n.t('appoint_as_mod')}
506 <span class="d-inline-block mr-1">
507 {i18n.t('are_you_sure')}
510 class="pointer d-inline-block mr-1"
513 this.handleAddModToCommunity
519 class="pointer d-inline-block"
522 this.handleCancelConfirmAppointAsMod
533 {/* Community creators and admins can transfer community to another mod */}
534 {(this.amCommunityCreator || this.canAdmin) &&
536 <li className="list-inline-item">
537 {!this.state.showConfirmTransferCommunity ? (
542 this.handleShowConfirmTransferCommunity
545 {i18n.t('transfer_community')}
549 <span class="d-inline-block mr-1">
550 {i18n.t('are_you_sure')}
553 class="pointer d-inline-block mr-1"
556 this.handleTransferCommunity
562 class="pointer d-inline-block"
566 .handleCancelShowConfirmTransferCommunity
575 {/* Admins can ban from all, and appoint other admins */}
579 <li className="list-inline-item">
580 {!node.comment.banned ? (
585 this.handleModBanShow
588 {i18n.t('ban_from_site')}
595 this.handleModBanSubmit
598 {i18n.t('unban_from_site')}
603 {!node.comment.banned && (
604 <li className="list-inline-item">
605 {!this.state.showConfirmAppointAsAdmin ? (
610 this.handleShowConfirmAppointAsAdmin
614 ? i18n.t('remove_as_admin')
615 : i18n.t('appoint_as_admin')}
619 <span class="d-inline-block mr-1">
620 {i18n.t('are_you_sure')}
623 class="pointer d-inline-block mr-1"
632 class="pointer d-inline-block"
635 this.handleCancelConfirmAppointAsAdmin
646 {/* Site Creator can transfer to another admin */}
647 {this.amSiteCreator && this.isAdmin && (
648 <li className="list-inline-item">
649 {!this.state.showConfirmTransferSite ? (
654 this.handleShowConfirmTransferSite
657 {i18n.t('transfer_site')}
661 <span class="d-inline-block mr-1">
662 {i18n.t('are_you_sure')}
665 class="pointer d-inline-block mr-1"
668 this.handleTransferSite
674 class="pointer d-inline-block"
677 this.handleCancelShowConfirmTransferSite
695 {/* end of details */}
696 {this.state.showRemoveDialog && (
699 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
703 class="form-control mr-2"
704 placeholder={i18n.t('reason')}
705 value={this.state.removeReason}
706 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
708 <button type="submit" class="btn btn-secondary">
709 {i18n.t('remove_comment')}
713 {this.state.showBanDialog && (
714 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
715 <div class="form-group row">
716 <label class="col-form-label">{i18n.t('reason')}</label>
719 class="form-control mr-2"
720 placeholder={i18n.t('reason')}
721 value={this.state.banReason}
722 onInput={linkEvent(this, this.handleModBanReasonChange)}
725 {/* TODO hold off on expires until later */}
726 {/* <div class="form-group row"> */}
727 {/* <label class="col-form-label">Expires</label> */}
728 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
730 <div class="form-group row">
731 <button type="submit" class="btn btn-secondary">
732 {i18n.t('ban')} {node.comment.creator_name}
737 {this.state.showReply && (
740 onReplyCancel={this.handleReplyCancel}
741 disabled={this.props.locked}
744 {node.children && !this.state.collapsed && (
746 nodes={node.children}
747 locked={this.props.locked}
748 moderators={this.props.moderators}
749 admins={this.props.admins}
750 postCreatorId={this.props.postCreatorId}
751 sort={this.props.sort}
752 sortType={this.props.sortType}
755 {/* A collapsed clearfix */}
756 {this.state.collapsed && <div class="row col-12"></div>}
762 let node = this.props.node;
764 <li className="list-inline-item">
766 className="btn btn-link btn-animate text-muted"
767 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
769 this.props.showContext ? i18n.t('show_context') : i18n.t('link')
772 <svg class="icon icon-inline">
773 <use xlinkHref="#icon-link"></use>
782 <svg class="icon icon-spinner spin">
783 <use xlinkHref="#icon-spinner"></use>
788 get myComment(): boolean {
790 UserService.Instance.user &&
791 this.props.node.comment.creator_id == UserService.Instance.user.id
795 get isMod(): boolean {
797 this.props.moderators &&
799 this.props.moderators.map(m => m.user_id),
800 this.props.node.comment.creator_id
805 get isAdmin(): boolean {
809 this.props.admins.map(a => a.id),
810 this.props.node.comment.creator_id
815 get isPostCreator(): boolean {
816 return this.props.node.comment.creator_id == this.props.postCreatorId;
819 get canMod(): boolean {
820 if (this.props.admins && this.props.moderators) {
821 let adminsThenMods = this.props.admins
823 .concat(this.props.moderators.map(m => m.user_id));
826 UserService.Instance.user,
828 this.props.node.comment.creator_id
835 get canAdmin(): boolean {
839 UserService.Instance.user,
840 this.props.admins.map(a => a.id),
841 this.props.node.comment.creator_id
846 get amCommunityCreator(): boolean {
848 this.props.moderators &&
849 UserService.Instance.user &&
850 this.props.node.comment.creator_id != UserService.Instance.user.id &&
851 UserService.Instance.user.id == this.props.moderators[0].user_id
855 get amSiteCreator(): boolean {
858 UserService.Instance.user &&
859 this.props.node.comment.creator_id != UserService.Instance.user.id &&
860 UserService.Instance.user.id == this.props.admins[0].id
864 get commentUnlessRemoved(): string {
865 let node = this.props.node;
866 return node.comment.removed
867 ? `*${i18n.t('removed')}*`
868 : node.comment.deleted
869 ? `*${i18n.t('deleted')}*`
870 : node.comment.content;
873 handleReplyClick(i: CommentNode) {
874 i.state.showReply = true;
878 handleEditClick(i: CommentNode) {
879 i.state.showEdit = true;
883 handleDeleteClick(i: CommentNode) {
884 let deleteForm: CommentFormI = {
885 content: i.props.node.comment.content,
886 edit_id: i.props.node.comment.id,
887 creator_id: i.props.node.comment.creator_id,
888 post_id: i.props.node.comment.post_id,
889 parent_id: i.props.node.comment.parent_id,
890 deleted: !i.props.node.comment.deleted,
893 WebSocketService.Instance.editComment(deleteForm);
896 handleSaveCommentClick(i: CommentNode) {
898 i.props.node.comment.saved == undefined
900 : !i.props.node.comment.saved;
901 let form: SaveCommentForm = {
902 comment_id: i.props.node.comment.id,
906 WebSocketService.Instance.saveComment(form);
908 i.state.saveLoading = true;
909 i.setState(this.state);
912 handleReplyCancel() {
913 this.state.showReply = false;
914 this.state.showEdit = false;
915 this.setState(this.state);
918 handleCommentUpvote(i: CommentNodeI) {
919 let new_vote = this.state.my_vote == 1 ? 0 : 1;
921 if (this.state.my_vote == 1) {
923 this.state.upvotes--;
924 } else if (this.state.my_vote == -1) {
925 this.state.downvotes--;
926 this.state.upvotes++;
927 this.state.score += 2;
929 this.state.upvotes++;
933 this.state.my_vote = new_vote;
935 let form: CommentLikeForm = {
936 comment_id: i.comment.id,
937 post_id: i.comment.post_id,
938 score: this.state.my_vote,
941 WebSocketService.Instance.likeComment(form);
942 this.setState(this.state);
946 handleCommentDownvote(i: CommentNodeI) {
947 let new_vote = this.state.my_vote == -1 ? 0 : -1;
949 if (this.state.my_vote == 1) {
950 this.state.score -= 2;
951 this.state.upvotes--;
952 this.state.downvotes++;
953 } else if (this.state.my_vote == -1) {
954 this.state.downvotes--;
957 this.state.downvotes++;
961 this.state.my_vote = new_vote;
963 let form: CommentLikeForm = {
964 comment_id: i.comment.id,
965 post_id: i.comment.post_id,
966 score: this.state.my_vote,
969 WebSocketService.Instance.likeComment(form);
970 this.setState(this.state);
974 handleModRemoveShow(i: CommentNode) {
975 i.state.showRemoveDialog = true;
979 handleModRemoveReasonChange(i: CommentNode, event: any) {
980 i.state.removeReason = event.target.value;
984 handleModRemoveSubmit(i: CommentNode) {
985 event.preventDefault();
986 let form: CommentFormI = {
987 content: i.props.node.comment.content,
988 edit_id: i.props.node.comment.id,
989 creator_id: i.props.node.comment.creator_id,
990 post_id: i.props.node.comment.post_id,
991 parent_id: i.props.node.comment.parent_id,
992 removed: !i.props.node.comment.removed,
993 reason: i.state.removeReason,
996 WebSocketService.Instance.editComment(form);
998 i.state.showRemoveDialog = false;
1002 handleMarkRead(i: CommentNode) {
1003 // if it has a user_mention_id field, then its a mention
1004 if (i.props.node.comment.user_mention_id) {
1005 let form: EditUserMentionForm = {
1006 user_mention_id: i.props.node.comment.user_mention_id,
1007 read: !i.props.node.comment.read,
1009 WebSocketService.Instance.editUserMention(form);
1011 let form: CommentFormI = {
1012 content: i.props.node.comment.content,
1013 edit_id: i.props.node.comment.id,
1014 creator_id: i.props.node.comment.creator_id,
1015 post_id: i.props.node.comment.post_id,
1016 parent_id: i.props.node.comment.parent_id,
1017 read: !i.props.node.comment.read,
1020 WebSocketService.Instance.editComment(form);
1023 i.state.readLoading = true;
1024 i.setState(this.state);
1027 handleModBanFromCommunityShow(i: CommentNode) {
1028 i.state.showBanDialog = !i.state.showBanDialog;
1029 i.state.banType = BanType.Community;
1030 i.setState(i.state);
1033 handleModBanShow(i: CommentNode) {
1034 i.state.showBanDialog = !i.state.showBanDialog;
1035 i.state.banType = BanType.Site;
1036 i.setState(i.state);
1039 handleModBanReasonChange(i: CommentNode, event: any) {
1040 i.state.banReason = event.target.value;
1041 i.setState(i.state);
1044 handleModBanExpiresChange(i: CommentNode, event: any) {
1045 i.state.banExpires = event.target.value;
1046 i.setState(i.state);
1049 handleModBanFromCommunitySubmit(i: CommentNode) {
1050 i.state.banType = BanType.Community;
1051 i.setState(i.state);
1052 i.handleModBanBothSubmit(i);
1055 handleModBanSubmit(i: CommentNode) {
1056 i.state.banType = BanType.Site;
1057 i.setState(i.state);
1058 i.handleModBanBothSubmit(i);
1061 handleModBanBothSubmit(i: CommentNode) {
1062 event.preventDefault();
1064 if (i.state.banType == BanType.Community) {
1065 let form: BanFromCommunityForm = {
1066 user_id: i.props.node.comment.creator_id,
1067 community_id: i.props.node.comment.community_id,
1068 ban: !i.props.node.comment.banned_from_community,
1069 reason: i.state.banReason,
1070 expires: getUnixTime(i.state.banExpires),
1072 WebSocketService.Instance.banFromCommunity(form);
1074 let form: BanUserForm = {
1075 user_id: i.props.node.comment.creator_id,
1076 ban: !i.props.node.comment.banned,
1077 reason: i.state.banReason,
1078 expires: getUnixTime(i.state.banExpires),
1080 WebSocketService.Instance.banUser(form);
1083 i.state.showBanDialog = false;
1084 i.setState(i.state);
1087 handleShowConfirmAppointAsMod(i: CommentNode) {
1088 i.state.showConfirmAppointAsMod = true;
1089 i.setState(i.state);
1092 handleCancelConfirmAppointAsMod(i: CommentNode) {
1093 i.state.showConfirmAppointAsMod = false;
1094 i.setState(i.state);
1097 handleAddModToCommunity(i: CommentNode) {
1098 let form: AddModToCommunityForm = {
1099 user_id: i.props.node.comment.creator_id,
1100 community_id: i.props.node.comment.community_id,
1103 WebSocketService.Instance.addModToCommunity(form);
1104 i.state.showConfirmAppointAsMod = false;
1105 i.setState(i.state);
1108 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1109 i.state.showConfirmAppointAsAdmin = true;
1110 i.setState(i.state);
1113 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1114 i.state.showConfirmAppointAsAdmin = false;
1115 i.setState(i.state);
1118 handleAddAdmin(i: CommentNode) {
1119 let form: AddAdminForm = {
1120 user_id: i.props.node.comment.creator_id,
1123 WebSocketService.Instance.addAdmin(form);
1124 i.state.showConfirmAppointAsAdmin = false;
1125 i.setState(i.state);
1128 handleShowConfirmTransferCommunity(i: CommentNode) {
1129 i.state.showConfirmTransferCommunity = true;
1130 i.setState(i.state);
1133 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1134 i.state.showConfirmTransferCommunity = false;
1135 i.setState(i.state);
1138 handleTransferCommunity(i: CommentNode) {
1139 let form: TransferCommunityForm = {
1140 community_id: i.props.node.comment.community_id,
1141 user_id: i.props.node.comment.creator_id,
1143 WebSocketService.Instance.transferCommunity(form);
1144 i.state.showConfirmTransferCommunity = false;
1145 i.setState(i.state);
1148 handleShowConfirmTransferSite(i: CommentNode) {
1149 i.state.showConfirmTransferSite = true;
1150 i.setState(i.state);
1153 handleCancelShowConfirmTransferSite(i: CommentNode) {
1154 i.state.showConfirmTransferSite = false;
1155 i.setState(i.state);
1158 handleTransferSite(i: CommentNode) {
1159 let form: TransferSiteForm = {
1160 user_id: i.props.node.comment.creator_id,
1162 WebSocketService.Instance.transferSite(form);
1163 i.state.showConfirmTransferSite = false;
1164 i.setState(i.state);
1167 get isCommentNew(): boolean {
1168 let now = moment.utc().subtract(10, 'minutes');
1169 let then = moment.utc(this.props.node.comment.published);
1170 return now.isBefore(then);
1173 handleCommentCollapse(i: CommentNode) {
1174 i.state.collapsed = !i.state.collapsed;
1175 i.setState(i.state);
1178 handleViewSource(i: CommentNode) {
1179 i.state.viewSource = !i.state.viewSource;
1180 i.setState(i.state);
1183 handleShowAdvanced(i: CommentNode) {
1184 i.state.showAdvanced = !i.state.showAdvanced;
1185 i.setState(i.state);
1190 if (this.state.my_vote == 1) {
1192 } else if (this.state.my_vote == -1) {
1193 return 'text-danger';
1195 return 'text-muted';
1199 get pointsTippy(): string {
1200 let points = i18n.t('number_of_points', {
1201 count: this.state.score,
1204 let upvotes = i18n.t('number_of_upvotes', {
1205 count: this.state.upvotes,
1208 let downvotes = i18n.t('number_of_downvotes', {
1209 count: this.state.downvotes,
1212 return `${points} • ${upvotes} • ${downvotes}`;