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,
161 published: node.comment.creator_published,
167 <div className="badge badge-light d-none d-sm-inline mr-2">
172 <div className="badge badge-light d-none d-sm-inline mr-2">
176 {this.isPostCreator && (
177 <div className="badge badge-light d-none d-sm-inline mr-2">
181 {(node.comment.banned_from_community || node.comment.banned) && (
182 <div className="badge badge-danger mr-2">
186 {this.props.showCommunity && (
188 <span class="mx-1">{i18n.t('to')}</span>
189 <Link class="mr-2" to={`/c/${node.comment.community_name}`}>
190 {node.comment.community_name}
195 class="btn btn-sm text-muted"
196 onClick={linkEvent(this, this.handleCommentCollapse)}
198 {this.state.collapsed ? (
199 <svg class="icon icon-inline">
200 <use xlinkHref="#icon-plus-square"></use>
203 <svg class="icon icon-inline">
204 <use xlinkHref="#icon-minus-square"></use>
208 {/* This is an expanding spacer for mobile */}
209 <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
211 className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
212 onClick={linkEvent(node, this.handleCommentUpvote)}
213 data-tippy-content={this.pointsTippy}
215 <svg class="icon icon-inline mr-1">
216 <use xlinkHref="#icon-zap"></use>
218 <span class="mr-1">{this.state.score}</span>
220 <span className="mr-1">•</span>
222 <MomentTime data={node.comment} />
225 {this.props.showCommunity && !this.state.collapsed && (
227 <div class="md-div text-muted small">
228 <Link class="mr-2" to={`/post/${node.comment.post_id}`}>
229 {node.comment.post_name}
234 {/* end of user row */}
235 {this.state.showEdit && (
239 onReplyCancel={this.handleReplyCancel}
240 disabled={this.props.locked}
244 {!this.state.showEdit && !this.state.collapsed && (
246 {this.state.viewSource ? (
247 <pre>{this.commentUnlessRemoved}</pre>
251 dangerouslySetInnerHTML={mdToHtml(
252 this.commentUnlessRemoved
256 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
257 {this.props.showContext && this.linkBtn}
258 {this.props.markable && (
260 class="btn btn-link btn-animate text-muted"
261 onClick={linkEvent(this, this.handleMarkRead)}
264 ? i18n.t('mark_as_unread')
265 : i18n.t('mark_as_read')
268 {this.state.readLoading ? (
272 class={`icon icon-inline ${
273 node.comment.read && 'text-success'
276 <use xlinkHref="#icon-check"></use>
281 {UserService.Instance.user && !this.props.viewOnly && (
284 className={`btn btn-link btn-animate ${
285 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
287 onClick={linkEvent(node, this.handleCommentUpvote)}
288 data-tippy-content={i18n.t('upvote')}
290 <svg class="icon icon-inline">
291 <use xlinkHref="#icon-arrow-up"></use>
293 {this.state.upvotes !== this.state.score && (
294 <span class="ml-1">{this.state.upvotes}</span>
297 {this.props.enableDownvotes && (
299 className={`btn btn-link btn-animate ${
300 this.state.my_vote == -1
304 onClick={linkEvent(node, this.handleCommentDownvote)}
305 data-tippy-content={i18n.t('downvote')}
307 <svg class="icon icon-inline">
308 <use xlinkHref="#icon-arrow-down"></use>
310 {this.state.upvotes !== this.state.score && (
311 <span class="ml-1">{this.state.downvotes}</span>
316 class="btn btn-link btn-animate text-muted"
317 onClick={linkEvent(this, this.handleReplyClick)}
318 data-tippy-content={i18n.t('reply')}
320 <svg class="icon icon-inline">
321 <use xlinkHref="#icon-reply1"></use>
324 {!this.state.showAdvanced ? (
326 className="btn btn-link btn-animate text-muted"
327 onClick={linkEvent(this, this.handleShowAdvanced)}
328 data-tippy-content={i18n.t('more')}
330 <svg class="icon icon-inline">
331 <use xlinkHref="#icon-more-vertical"></use>
336 {!this.myComment && (
337 <button class="btn btn-link btn-animate">
340 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
341 title={i18n.t('message').toLowerCase()}
344 <use xlinkHref="#icon-mail"></use>
349 {!this.props.showContext && this.linkBtn}
351 class="btn btn-link btn-animate text-muted"
354 this.handleSaveCommentClick
362 {this.state.saveLoading ? (
366 class={`icon icon-inline ${
367 node.comment.saved && 'text-warning'
370 <use xlinkHref="#icon-star"></use>
375 className="btn btn-link btn-animate text-muted"
376 onClick={linkEvent(this, this.handleViewSource)}
377 data-tippy-content={i18n.t('view_source')}
380 class={`icon icon-inline ${
381 this.state.viewSource && 'text-success'
384 <use xlinkHref="#icon-file-text"></use>
390 class="btn btn-link btn-animate text-muted"
391 onClick={linkEvent(this, this.handleEditClick)}
392 data-tippy-content={i18n.t('edit')}
394 <svg class="icon icon-inline">
395 <use xlinkHref="#icon-edit"></use>
399 class="btn btn-link btn-animate text-muted"
402 this.handleDeleteClick
405 !node.comment.deleted
411 class={`icon icon-inline ${
412 node.comment.deleted && 'text-danger'
415 <use xlinkHref="#icon-trash"></use>
420 {/* Admins and mods can remove comments */}
421 {(this.canMod || this.canAdmin) && (
423 {!node.comment.removed ? (
425 class="btn btn-link btn-animate text-muted"
428 this.handleModRemoveShow
435 class="btn btn-link btn-animate text-muted"
438 this.handleModRemoveSubmit
446 {/* Mods can ban from community, and appoint as mods to community */}
450 (!node.comment.banned_from_community ? (
452 class="btn btn-link btn-animate text-muted"
455 this.handleModBanFromCommunityShow
462 class="btn btn-link btn-animate text-muted"
465 this.handleModBanFromCommunitySubmit
471 {!node.comment.banned_from_community &&
472 (!this.state.showConfirmAppointAsMod ? (
474 class="btn btn-link btn-animate text-muted"
477 this.handleShowConfirmAppointAsMod
481 ? i18n.t('remove_as_mod')
482 : i18n.t('appoint_as_mod')}
486 <button class="btn btn-link btn-animate text-muted">
487 {i18n.t('are_you_sure')}
490 class="btn btn-link btn-animate text-muted"
493 this.handleAddModToCommunity
499 class="btn btn-link btn-animate text-muted"
502 this.handleCancelConfirmAppointAsMod
511 {/* Community creators and admins can transfer community to another mod */}
512 {(this.amCommunityCreator || this.canAdmin) &&
514 (!this.state.showConfirmTransferCommunity ? (
516 class="btn btn-link btn-animate text-muted"
519 this.handleShowConfirmTransferCommunity
522 {i18n.t('transfer_community')}
526 <button class="btn btn-link btn-animate text-muted">
527 {i18n.t('are_you_sure')}
530 class="btn btn-link btn-animate text-muted"
533 this.handleTransferCommunity
539 class="btn btn-link btn-animate text-muted"
543 .handleCancelShowConfirmTransferCommunity
550 {/* Admins can ban from all, and appoint other admins */}
554 (!node.comment.banned ? (
556 class="btn btn-link btn-animate text-muted"
559 this.handleModBanShow
562 {i18n.t('ban_from_site')}
566 class="btn btn-link btn-animate text-muted"
569 this.handleModBanSubmit
572 {i18n.t('unban_from_site')}
575 {!node.comment.banned &&
576 (!this.state.showConfirmAppointAsAdmin ? (
578 class="btn btn-link btn-animate text-muted"
581 this.handleShowConfirmAppointAsAdmin
585 ? i18n.t('remove_as_admin')
586 : i18n.t('appoint_as_admin')}
590 <button class="btn btn-link btn-animate text-muted">
591 {i18n.t('are_you_sure')}
594 class="btn btn-link btn-animate text-muted"
603 class="btn btn-link btn-animate text-muted"
606 this.handleCancelConfirmAppointAsAdmin
615 {/* Site Creator can transfer to another admin */}
616 {this.amSiteCreator &&
618 (!this.state.showConfirmTransferSite ? (
620 class="btn btn-link btn-animate text-muted"
623 this.handleShowConfirmTransferSite
626 {i18n.t('transfer_site')}
630 <button class="btn btn-link btn-animate text-muted">
631 {i18n.t('are_you_sure')}
634 class="btn btn-link btn-animate text-muted"
637 this.handleTransferSite
643 class="btn btn-link btn-animate text-muted"
646 this.handleCancelShowConfirmTransferSite
658 {/* end of button group */}
663 {/* end of details */}
664 {this.state.showRemoveDialog && (
667 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
671 class="form-control mr-2"
672 placeholder={i18n.t('reason')}
673 value={this.state.removeReason}
674 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
676 <button type="submit" class="btn btn-secondary">
677 {i18n.t('remove_comment')}
681 {this.state.showBanDialog && (
682 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
683 <div class="form-group row">
684 <label class="col-form-label">{i18n.t('reason')}</label>
687 class="form-control mr-2"
688 placeholder={i18n.t('reason')}
689 value={this.state.banReason}
690 onInput={linkEvent(this, this.handleModBanReasonChange)}
693 {/* TODO hold off on expires until later */}
694 {/* <div class="form-group row"> */}
695 {/* <label class="col-form-label">Expires</label> */}
696 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
698 <div class="form-group row">
699 <button type="submit" class="btn btn-secondary">
700 {i18n.t('ban')} {node.comment.creator_name}
705 {this.state.showReply && (
708 onReplyCancel={this.handleReplyCancel}
709 disabled={this.props.locked}
713 {node.children && !this.state.collapsed && (
715 nodes={node.children}
716 locked={this.props.locked}
717 moderators={this.props.moderators}
718 admins={this.props.admins}
719 postCreatorId={this.props.postCreatorId}
720 sort={this.props.sort}
721 sortType={this.props.sortType}
722 enableDownvotes={this.props.enableDownvotes}
725 {/* A collapsed clearfix */}
726 {this.state.collapsed && <div class="row col-12"></div>}
732 let node = this.props.node;
735 class="btn btn-link btn-animate text-muted"
736 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
737 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
739 <svg class="icon icon-inline">
740 <use xlinkHref="#icon-link"></use>
748 <svg class="icon icon-spinner spin">
749 <use xlinkHref="#icon-spinner"></use>
754 get myComment(): boolean {
756 UserService.Instance.user &&
757 this.props.node.comment.creator_id == UserService.Instance.user.id
761 get isMod(): boolean {
763 this.props.moderators &&
765 this.props.moderators.map(m => m.user_id),
766 this.props.node.comment.creator_id
771 get isAdmin(): boolean {
775 this.props.admins.map(a => a.id),
776 this.props.node.comment.creator_id
781 get isPostCreator(): boolean {
782 return this.props.node.comment.creator_id == this.props.postCreatorId;
785 get canMod(): boolean {
786 if (this.props.admins && this.props.moderators) {
787 let adminsThenMods = this.props.admins
789 .concat(this.props.moderators.map(m => m.user_id));
792 UserService.Instance.user,
794 this.props.node.comment.creator_id
801 get canAdmin(): boolean {
805 UserService.Instance.user,
806 this.props.admins.map(a => a.id),
807 this.props.node.comment.creator_id
812 get amCommunityCreator(): boolean {
814 this.props.moderators &&
815 UserService.Instance.user &&
816 this.props.node.comment.creator_id != UserService.Instance.user.id &&
817 UserService.Instance.user.id == this.props.moderators[0].user_id
821 get amSiteCreator(): boolean {
824 UserService.Instance.user &&
825 this.props.node.comment.creator_id != UserService.Instance.user.id &&
826 UserService.Instance.user.id == this.props.admins[0].id
830 get commentUnlessRemoved(): string {
831 let node = this.props.node;
832 return node.comment.removed
833 ? `*${i18n.t('removed')}*`
834 : node.comment.deleted
835 ? `*${i18n.t('deleted')}*`
836 : node.comment.content;
839 handleReplyClick(i: CommentNode) {
840 i.state.showReply = true;
844 handleEditClick(i: CommentNode) {
845 i.state.showEdit = true;
849 handleDeleteClick(i: CommentNode) {
850 let deleteForm: CommentFormI = {
851 content: i.props.node.comment.content,
852 edit_id: i.props.node.comment.id,
853 creator_id: i.props.node.comment.creator_id,
854 post_id: i.props.node.comment.post_id,
855 parent_id: i.props.node.comment.parent_id,
856 deleted: !i.props.node.comment.deleted,
859 WebSocketService.Instance.editComment(deleteForm);
862 handleSaveCommentClick(i: CommentNode) {
864 i.props.node.comment.saved == undefined
866 : !i.props.node.comment.saved;
867 let form: SaveCommentForm = {
868 comment_id: i.props.node.comment.id,
872 WebSocketService.Instance.saveComment(form);
874 i.state.saveLoading = true;
875 i.setState(this.state);
878 handleReplyCancel() {
879 this.state.showReply = false;
880 this.state.showEdit = false;
881 this.setState(this.state);
884 handleCommentUpvote(i: CommentNodeI) {
885 let new_vote = this.state.my_vote == 1 ? 0 : 1;
887 if (this.state.my_vote == 1) {
889 this.state.upvotes--;
890 } else if (this.state.my_vote == -1) {
891 this.state.downvotes--;
892 this.state.upvotes++;
893 this.state.score += 2;
895 this.state.upvotes++;
899 this.state.my_vote = new_vote;
901 let form: CommentLikeForm = {
902 comment_id: i.comment.id,
903 post_id: i.comment.post_id,
904 score: this.state.my_vote,
907 WebSocketService.Instance.likeComment(form);
908 this.setState(this.state);
912 handleCommentDownvote(i: CommentNodeI) {
913 let new_vote = this.state.my_vote == -1 ? 0 : -1;
915 if (this.state.my_vote == 1) {
916 this.state.score -= 2;
917 this.state.upvotes--;
918 this.state.downvotes++;
919 } else if (this.state.my_vote == -1) {
920 this.state.downvotes--;
923 this.state.downvotes++;
927 this.state.my_vote = new_vote;
929 let form: CommentLikeForm = {
930 comment_id: i.comment.id,
931 post_id: i.comment.post_id,
932 score: this.state.my_vote,
935 WebSocketService.Instance.likeComment(form);
936 this.setState(this.state);
940 handleModRemoveShow(i: CommentNode) {
941 i.state.showRemoveDialog = true;
945 handleModRemoveReasonChange(i: CommentNode, event: any) {
946 i.state.removeReason = event.target.value;
950 handleModRemoveSubmit(i: CommentNode) {
951 event.preventDefault();
952 let form: CommentFormI = {
953 content: i.props.node.comment.content,
954 edit_id: i.props.node.comment.id,
955 creator_id: i.props.node.comment.creator_id,
956 post_id: i.props.node.comment.post_id,
957 parent_id: i.props.node.comment.parent_id,
958 removed: !i.props.node.comment.removed,
959 reason: i.state.removeReason,
962 WebSocketService.Instance.editComment(form);
964 i.state.showRemoveDialog = false;
968 handleMarkRead(i: CommentNode) {
969 // if it has a user_mention_id field, then its a mention
970 if (i.props.node.comment.user_mention_id) {
971 let form: EditUserMentionForm = {
972 user_mention_id: i.props.node.comment.user_mention_id,
973 read: !i.props.node.comment.read,
975 WebSocketService.Instance.editUserMention(form);
977 let form: CommentFormI = {
978 content: i.props.node.comment.content,
979 edit_id: i.props.node.comment.id,
980 creator_id: i.props.node.comment.creator_id,
981 post_id: i.props.node.comment.post_id,
982 parent_id: i.props.node.comment.parent_id,
983 read: !i.props.node.comment.read,
986 WebSocketService.Instance.editComment(form);
989 i.state.readLoading = true;
990 i.setState(this.state);
993 handleModBanFromCommunityShow(i: CommentNode) {
994 i.state.showBanDialog = !i.state.showBanDialog;
995 i.state.banType = BanType.Community;
999 handleModBanShow(i: CommentNode) {
1000 i.state.showBanDialog = !i.state.showBanDialog;
1001 i.state.banType = BanType.Site;
1002 i.setState(i.state);
1005 handleModBanReasonChange(i: CommentNode, event: any) {
1006 i.state.banReason = event.target.value;
1007 i.setState(i.state);
1010 handleModBanExpiresChange(i: CommentNode, event: any) {
1011 i.state.banExpires = event.target.value;
1012 i.setState(i.state);
1015 handleModBanFromCommunitySubmit(i: CommentNode) {
1016 i.state.banType = BanType.Community;
1017 i.setState(i.state);
1018 i.handleModBanBothSubmit(i);
1021 handleModBanSubmit(i: CommentNode) {
1022 i.state.banType = BanType.Site;
1023 i.setState(i.state);
1024 i.handleModBanBothSubmit(i);
1027 handleModBanBothSubmit(i: CommentNode) {
1028 event.preventDefault();
1030 if (i.state.banType == BanType.Community) {
1031 let form: BanFromCommunityForm = {
1032 user_id: i.props.node.comment.creator_id,
1033 community_id: i.props.node.comment.community_id,
1034 ban: !i.props.node.comment.banned_from_community,
1035 reason: i.state.banReason,
1036 expires: getUnixTime(i.state.banExpires),
1038 WebSocketService.Instance.banFromCommunity(form);
1040 let form: BanUserForm = {
1041 user_id: i.props.node.comment.creator_id,
1042 ban: !i.props.node.comment.banned,
1043 reason: i.state.banReason,
1044 expires: getUnixTime(i.state.banExpires),
1046 WebSocketService.Instance.banUser(form);
1049 i.state.showBanDialog = false;
1050 i.setState(i.state);
1053 handleShowConfirmAppointAsMod(i: CommentNode) {
1054 i.state.showConfirmAppointAsMod = true;
1055 i.setState(i.state);
1058 handleCancelConfirmAppointAsMod(i: CommentNode) {
1059 i.state.showConfirmAppointAsMod = false;
1060 i.setState(i.state);
1063 handleAddModToCommunity(i: CommentNode) {
1064 let form: AddModToCommunityForm = {
1065 user_id: i.props.node.comment.creator_id,
1066 community_id: i.props.node.comment.community_id,
1069 WebSocketService.Instance.addModToCommunity(form);
1070 i.state.showConfirmAppointAsMod = false;
1071 i.setState(i.state);
1074 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1075 i.state.showConfirmAppointAsAdmin = true;
1076 i.setState(i.state);
1079 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1080 i.state.showConfirmAppointAsAdmin = false;
1081 i.setState(i.state);
1084 handleAddAdmin(i: CommentNode) {
1085 let form: AddAdminForm = {
1086 user_id: i.props.node.comment.creator_id,
1089 WebSocketService.Instance.addAdmin(form);
1090 i.state.showConfirmAppointAsAdmin = false;
1091 i.setState(i.state);
1094 handleShowConfirmTransferCommunity(i: CommentNode) {
1095 i.state.showConfirmTransferCommunity = true;
1096 i.setState(i.state);
1099 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1100 i.state.showConfirmTransferCommunity = false;
1101 i.setState(i.state);
1104 handleTransferCommunity(i: CommentNode) {
1105 let form: TransferCommunityForm = {
1106 community_id: i.props.node.comment.community_id,
1107 user_id: i.props.node.comment.creator_id,
1109 WebSocketService.Instance.transferCommunity(form);
1110 i.state.showConfirmTransferCommunity = false;
1111 i.setState(i.state);
1114 handleShowConfirmTransferSite(i: CommentNode) {
1115 i.state.showConfirmTransferSite = true;
1116 i.setState(i.state);
1119 handleCancelShowConfirmTransferSite(i: CommentNode) {
1120 i.state.showConfirmTransferSite = false;
1121 i.setState(i.state);
1124 handleTransferSite(i: CommentNode) {
1125 let form: TransferSiteForm = {
1126 user_id: i.props.node.comment.creator_id,
1128 WebSocketService.Instance.transferSite(form);
1129 i.state.showConfirmTransferSite = false;
1130 i.setState(i.state);
1133 get isCommentNew(): boolean {
1134 let now = moment.utc().subtract(10, 'minutes');
1135 let then = moment.utc(this.props.node.comment.published);
1136 return now.isBefore(then);
1139 handleCommentCollapse(i: CommentNode) {
1140 i.state.collapsed = !i.state.collapsed;
1141 i.setState(i.state);
1144 handleViewSource(i: CommentNode) {
1145 i.state.viewSource = !i.state.viewSource;
1146 i.setState(i.state);
1149 handleShowAdvanced(i: CommentNode) {
1150 i.state.showAdvanced = !i.state.showAdvanced;
1151 i.setState(i.state);
1156 if (this.state.my_vote == 1) {
1158 } else if (this.state.my_vote == -1) {
1159 return 'text-danger';
1161 return 'text-muted';
1165 get pointsTippy(): string {
1166 let points = i18n.t('number_of_points', {
1167 count: this.state.score,
1170 let upvotes = i18n.t('number_of_upvotes', {
1171 count: this.state.upvotes,
1174 let downvotes = i18n.t('number_of_downvotes', {
1175 count: this.state.downvotes,
1178 return `${points} • ${upvotes} • ${downvotes}`;