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 { CommunityLink } from './community-link';
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;
77 enableDownvotes: boolean;
80 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
81 private emptyState: CommentNodeState = {
84 showRemoveDialog: false,
89 banType: BanType.Community,
93 showConfirmTransferSite: false,
94 showConfirmTransferCommunity: false,
95 showConfirmAppointAsMod: false,
96 showConfirmAppointAsAdmin: false,
97 my_vote: this.props.node.comment.my_vote,
98 score: this.props.node.comment.score,
99 upvotes: this.props.node.comment.upvotes,
100 downvotes: this.props.node.comment.downvotes,
101 borderColor: this.props.node.comment.depth
102 ? colorList[this.props.node.comment.depth % colorList.length]
108 constructor(props: any, context: any) {
109 super(props, context);
111 this.state = this.emptyState;
112 this.handleReplyCancel = this.handleReplyCancel.bind(this);
113 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
114 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
117 componentWillReceiveProps(nextProps: CommentNodeProps) {
118 this.state.my_vote = nextProps.node.comment.my_vote;
119 this.state.upvotes = nextProps.node.comment.upvotes;
120 this.state.downvotes = nextProps.node.comment.downvotes;
121 this.state.score = nextProps.node.comment.score;
122 this.state.readLoading = false;
123 this.state.saveLoading = false;
124 this.setState(this.state);
128 let node = this.props.node;
131 className={`comment ${
132 node.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
136 id={`comment-${node.comment.id}`}
137 className={`details comment-node border-top border-light py-2 ${
138 this.isCommentNew ? 'mark' : ''
141 !this.props.noIndent &&
142 this.props.node.comment.parent_id &&
143 `border-left: 2px ${this.state.borderColor} solid !important`
148 !this.props.noIndent &&
149 this.props.node.comment.parent_id &&
153 <div class="d-flex flex-wrap align-items-center text-muted small">
157 name: node.comment.creator_name,
158 avatar: node.comment.creator_avatar,
159 id: node.comment.creator_id,
160 local: node.comment.creator_local,
161 actor_id: node.comment.creator_actor_id,
162 published: node.comment.creator_published,
168 <div className="badge badge-light d-none d-sm-inline mr-2">
173 <div className="badge badge-light d-none d-sm-inline mr-2">
177 {this.isPostCreator && (
178 <div className="badge badge-light d-none d-sm-inline mr-2">
182 {(node.comment.banned_from_community || node.comment.banned) && (
183 <div className="badge badge-danger mr-2">
187 {this.props.showCommunity && (
189 <span class="mx-1">{i18n.t('to')}</span>
192 name: node.comment.community_name,
193 id: node.comment.community_id,
194 local: node.comment.community_local,
195 actor_id: node.comment.community_actor_id,
198 <span class="mx-2">•</span>
199 <Link class="mr-2" to={`/post/${node.comment.post_id}`}>
200 {node.comment.post_name}
205 class="btn btn-sm text-muted"
206 onClick={linkEvent(this, this.handleCommentCollapse)}
208 {this.state.collapsed ? (
209 <svg class="icon icon-inline">
210 <use xlinkHref="#icon-plus-square"></use>
213 <svg class="icon icon-inline">
214 <use xlinkHref="#icon-minus-square"></use>
218 {/* This is an expanding spacer for mobile */}
219 <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
221 className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
222 onClick={linkEvent(node, this.handleCommentUpvote)}
223 data-tippy-content={this.pointsTippy}
225 <svg class="icon icon-inline mr-1">
226 <use xlinkHref="#icon-zap"></use>
228 <span class="mr-1">{this.state.score}</span>
230 <span className="mr-1">•</span>
232 <MomentTime data={node.comment} />
235 {/* end of user row */}
236 {this.state.showEdit && (
240 onReplyCancel={this.handleReplyCancel}
241 disabled={this.props.locked}
245 {!this.state.showEdit && !this.state.collapsed && (
247 {this.state.viewSource ? (
248 <pre>{this.commentUnlessRemoved}</pre>
252 dangerouslySetInnerHTML={mdToHtml(
253 this.commentUnlessRemoved
257 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
258 {this.props.showContext && this.linkBtn}
259 {this.props.markable && (
261 class="btn btn-link btn-animate text-muted"
262 onClick={linkEvent(this, this.handleMarkRead)}
265 ? i18n.t('mark_as_unread')
266 : i18n.t('mark_as_read')
269 {this.state.readLoading ? (
273 class={`icon icon-inline ${
274 node.comment.read && 'text-success'
277 <use xlinkHref="#icon-check"></use>
282 {UserService.Instance.user && !this.props.viewOnly && (
285 className={`btn btn-link btn-animate ${
286 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
288 onClick={linkEvent(node, this.handleCommentUpvote)}
289 data-tippy-content={i18n.t('upvote')}
291 <svg class="icon icon-inline">
292 <use xlinkHref="#icon-arrow-up"></use>
294 {this.state.upvotes !== this.state.score && (
295 <span class="ml-1">{this.state.upvotes}</span>
298 {this.props.enableDownvotes && (
300 className={`btn btn-link btn-animate ${
301 this.state.my_vote == -1
305 onClick={linkEvent(node, this.handleCommentDownvote)}
306 data-tippy-content={i18n.t('downvote')}
308 <svg class="icon icon-inline">
309 <use xlinkHref="#icon-arrow-down"></use>
311 {this.state.upvotes !== this.state.score && (
312 <span class="ml-1">{this.state.downvotes}</span>
317 class="btn btn-link btn-animate text-muted"
318 onClick={linkEvent(this, this.handleReplyClick)}
319 data-tippy-content={i18n.t('reply')}
321 <svg class="icon icon-inline">
322 <use xlinkHref="#icon-reply1"></use>
325 {!this.state.showAdvanced ? (
327 className="btn btn-link btn-animate text-muted"
328 onClick={linkEvent(this, this.handleShowAdvanced)}
329 data-tippy-content={i18n.t('more')}
331 <svg class="icon icon-inline">
332 <use xlinkHref="#icon-more-vertical"></use>
337 {!this.myComment && (
338 <button class="btn btn-link btn-animate">
341 to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
342 title={i18n.t('message').toLowerCase()}
345 <use xlinkHref="#icon-mail"></use>
350 {!this.props.showContext && this.linkBtn}
352 class="btn btn-link btn-animate text-muted"
355 this.handleSaveCommentClick
363 {this.state.saveLoading ? (
367 class={`icon icon-inline ${
368 node.comment.saved && 'text-warning'
371 <use xlinkHref="#icon-star"></use>
376 className="btn btn-link btn-animate text-muted"
377 onClick={linkEvent(this, this.handleViewSource)}
378 data-tippy-content={i18n.t('view_source')}
381 class={`icon icon-inline ${
382 this.state.viewSource && 'text-success'
385 <use xlinkHref="#icon-file-text"></use>
391 class="btn btn-link btn-animate text-muted"
392 onClick={linkEvent(this, this.handleEditClick)}
393 data-tippy-content={i18n.t('edit')}
395 <svg class="icon icon-inline">
396 <use xlinkHref="#icon-edit"></use>
400 class="btn btn-link btn-animate text-muted"
403 this.handleDeleteClick
406 !node.comment.deleted
412 class={`icon icon-inline ${
413 node.comment.deleted && 'text-danger'
416 <use xlinkHref="#icon-trash"></use>
421 {/* Admins and mods can remove comments */}
422 {(this.canMod || this.canAdmin) && (
424 {!node.comment.removed ? (
426 class="btn btn-link btn-animate text-muted"
429 this.handleModRemoveShow
436 class="btn btn-link btn-animate text-muted"
439 this.handleModRemoveSubmit
447 {/* Mods can ban from community, and appoint as mods to community */}
451 (!node.comment.banned_from_community ? (
453 class="btn btn-link btn-animate text-muted"
456 this.handleModBanFromCommunityShow
463 class="btn btn-link btn-animate text-muted"
466 this.handleModBanFromCommunitySubmit
472 {!node.comment.banned_from_community &&
473 (!this.state.showConfirmAppointAsMod ? (
475 class="btn btn-link btn-animate text-muted"
478 this.handleShowConfirmAppointAsMod
482 ? i18n.t('remove_as_mod')
483 : i18n.t('appoint_as_mod')}
487 <button class="btn btn-link btn-animate text-muted">
488 {i18n.t('are_you_sure')}
491 class="btn btn-link btn-animate text-muted"
494 this.handleAddModToCommunity
500 class="btn btn-link btn-animate text-muted"
503 this.handleCancelConfirmAppointAsMod
512 {/* Community creators and admins can transfer community to another mod */}
513 {(this.amCommunityCreator || this.canAdmin) &&
515 (!this.state.showConfirmTransferCommunity ? (
517 class="btn btn-link btn-animate text-muted"
520 this.handleShowConfirmTransferCommunity
523 {i18n.t('transfer_community')}
527 <button class="btn btn-link btn-animate text-muted">
528 {i18n.t('are_you_sure')}
531 class="btn btn-link btn-animate text-muted"
534 this.handleTransferCommunity
540 class="btn btn-link btn-animate text-muted"
544 .handleCancelShowConfirmTransferCommunity
551 {/* Admins can ban from all, and appoint other admins */}
555 (!node.comment.banned ? (
557 class="btn btn-link btn-animate text-muted"
560 this.handleModBanShow
563 {i18n.t('ban_from_site')}
567 class="btn btn-link btn-animate text-muted"
570 this.handleModBanSubmit
573 {i18n.t('unban_from_site')}
576 {!node.comment.banned &&
577 (!this.state.showConfirmAppointAsAdmin ? (
579 class="btn btn-link btn-animate text-muted"
582 this.handleShowConfirmAppointAsAdmin
586 ? i18n.t('remove_as_admin')
587 : i18n.t('appoint_as_admin')}
591 <button class="btn btn-link btn-animate text-muted">
592 {i18n.t('are_you_sure')}
595 class="btn btn-link btn-animate text-muted"
604 class="btn btn-link btn-animate text-muted"
607 this.handleCancelConfirmAppointAsAdmin
616 {/* Site Creator can transfer to another admin */}
617 {this.amSiteCreator &&
619 (!this.state.showConfirmTransferSite ? (
621 class="btn btn-link btn-animate text-muted"
624 this.handleShowConfirmTransferSite
627 {i18n.t('transfer_site')}
631 <button class="btn btn-link btn-animate text-muted">
632 {i18n.t('are_you_sure')}
635 class="btn btn-link btn-animate text-muted"
638 this.handleTransferSite
644 class="btn btn-link btn-animate text-muted"
647 this.handleCancelShowConfirmTransferSite
659 {/* end of button group */}
664 {/* end of details */}
665 {this.state.showRemoveDialog && (
668 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
672 class="form-control mr-2"
673 placeholder={i18n.t('reason')}
674 value={this.state.removeReason}
675 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
677 <button type="submit" class="btn btn-secondary">
678 {i18n.t('remove_comment')}
682 {this.state.showBanDialog && (
683 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
684 <div class="form-group row">
685 <label class="col-form-label">{i18n.t('reason')}</label>
688 class="form-control mr-2"
689 placeholder={i18n.t('reason')}
690 value={this.state.banReason}
691 onInput={linkEvent(this, this.handleModBanReasonChange)}
694 {/* TODO hold off on expires until later */}
695 {/* <div class="form-group row"> */}
696 {/* <label class="col-form-label">Expires</label> */}
697 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
699 <div class="form-group row">
700 <button type="submit" class="btn btn-secondary">
701 {i18n.t('ban')} {node.comment.creator_name}
706 {this.state.showReply && (
709 onReplyCancel={this.handleReplyCancel}
710 disabled={this.props.locked}
714 {node.children && !this.state.collapsed && (
716 nodes={node.children}
717 locked={this.props.locked}
718 moderators={this.props.moderators}
719 admins={this.props.admins}
720 postCreatorId={this.props.postCreatorId}
721 sort={this.props.sort}
722 sortType={this.props.sortType}
723 enableDownvotes={this.props.enableDownvotes}
726 {/* A collapsed clearfix */}
727 {this.state.collapsed && <div class="row col-12"></div>}
733 let node = this.props.node;
736 class="btn btn-link btn-animate text-muted"
737 to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
738 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
740 <svg class="icon icon-inline">
741 <use xlinkHref="#icon-link"></use>
749 <svg class="icon icon-spinner spin">
750 <use xlinkHref="#icon-spinner"></use>
755 get myComment(): boolean {
757 UserService.Instance.user &&
758 this.props.node.comment.creator_id == UserService.Instance.user.id
762 get isMod(): boolean {
764 this.props.moderators &&
766 this.props.moderators.map(m => m.user_id),
767 this.props.node.comment.creator_id
772 get isAdmin(): boolean {
776 this.props.admins.map(a => a.id),
777 this.props.node.comment.creator_id
782 get isPostCreator(): boolean {
783 return this.props.node.comment.creator_id == this.props.postCreatorId;
786 get canMod(): boolean {
787 if (this.props.admins && this.props.moderators) {
788 let adminsThenMods = this.props.admins
790 .concat(this.props.moderators.map(m => m.user_id));
793 UserService.Instance.user,
795 this.props.node.comment.creator_id
802 get canAdmin(): boolean {
806 UserService.Instance.user,
807 this.props.admins.map(a => a.id),
808 this.props.node.comment.creator_id
813 get amCommunityCreator(): boolean {
815 this.props.moderators &&
816 UserService.Instance.user &&
817 this.props.node.comment.creator_id != UserService.Instance.user.id &&
818 UserService.Instance.user.id == this.props.moderators[0].user_id
822 get amSiteCreator(): boolean {
825 UserService.Instance.user &&
826 this.props.node.comment.creator_id != UserService.Instance.user.id &&
827 UserService.Instance.user.id == this.props.admins[0].id
831 get commentUnlessRemoved(): string {
832 let node = this.props.node;
833 return node.comment.removed
834 ? `*${i18n.t('removed')}*`
835 : node.comment.deleted
836 ? `*${i18n.t('deleted')}*`
837 : node.comment.content;
840 handleReplyClick(i: CommentNode) {
841 i.state.showReply = true;
845 handleEditClick(i: CommentNode) {
846 i.state.showEdit = true;
850 handleDeleteClick(i: CommentNode) {
851 let deleteForm: CommentFormI = {
852 content: i.props.node.comment.content,
853 edit_id: i.props.node.comment.id,
854 creator_id: i.props.node.comment.creator_id,
855 post_id: i.props.node.comment.post_id,
856 parent_id: i.props.node.comment.parent_id,
857 deleted: !i.props.node.comment.deleted,
860 WebSocketService.Instance.editComment(deleteForm);
863 handleSaveCommentClick(i: CommentNode) {
865 i.props.node.comment.saved == undefined
867 : !i.props.node.comment.saved;
868 let form: SaveCommentForm = {
869 comment_id: i.props.node.comment.id,
873 WebSocketService.Instance.saveComment(form);
875 i.state.saveLoading = true;
876 i.setState(this.state);
879 handleReplyCancel() {
880 this.state.showReply = false;
881 this.state.showEdit = false;
882 this.setState(this.state);
885 handleCommentUpvote(i: CommentNodeI) {
886 let new_vote = this.state.my_vote == 1 ? 0 : 1;
888 if (this.state.my_vote == 1) {
890 this.state.upvotes--;
891 } else if (this.state.my_vote == -1) {
892 this.state.downvotes--;
893 this.state.upvotes++;
894 this.state.score += 2;
896 this.state.upvotes++;
900 this.state.my_vote = new_vote;
902 let form: CommentLikeForm = {
903 comment_id: i.comment.id,
904 post_id: i.comment.post_id,
905 score: this.state.my_vote,
908 WebSocketService.Instance.likeComment(form);
909 this.setState(this.state);
913 handleCommentDownvote(i: CommentNodeI) {
914 let new_vote = this.state.my_vote == -1 ? 0 : -1;
916 if (this.state.my_vote == 1) {
917 this.state.score -= 2;
918 this.state.upvotes--;
919 this.state.downvotes++;
920 } else if (this.state.my_vote == -1) {
921 this.state.downvotes--;
924 this.state.downvotes++;
928 this.state.my_vote = new_vote;
930 let form: CommentLikeForm = {
931 comment_id: i.comment.id,
932 post_id: i.comment.post_id,
933 score: this.state.my_vote,
936 WebSocketService.Instance.likeComment(form);
937 this.setState(this.state);
941 handleModRemoveShow(i: CommentNode) {
942 i.state.showRemoveDialog = true;
946 handleModRemoveReasonChange(i: CommentNode, event: any) {
947 i.state.removeReason = event.target.value;
951 handleModRemoveSubmit(i: CommentNode) {
952 event.preventDefault();
953 let form: CommentFormI = {
954 content: i.props.node.comment.content,
955 edit_id: i.props.node.comment.id,
956 creator_id: i.props.node.comment.creator_id,
957 post_id: i.props.node.comment.post_id,
958 parent_id: i.props.node.comment.parent_id,
959 removed: !i.props.node.comment.removed,
960 reason: i.state.removeReason,
963 WebSocketService.Instance.editComment(form);
965 i.state.showRemoveDialog = false;
969 handleMarkRead(i: CommentNode) {
970 // if it has a user_mention_id field, then its a mention
971 if (i.props.node.comment.user_mention_id) {
972 let form: EditUserMentionForm = {
973 user_mention_id: i.props.node.comment.user_mention_id,
974 read: !i.props.node.comment.read,
976 WebSocketService.Instance.editUserMention(form);
978 let form: CommentFormI = {
979 content: i.props.node.comment.content,
980 edit_id: i.props.node.comment.id,
981 creator_id: i.props.node.comment.creator_id,
982 post_id: i.props.node.comment.post_id,
983 parent_id: i.props.node.comment.parent_id,
984 read: !i.props.node.comment.read,
987 WebSocketService.Instance.editComment(form);
990 i.state.readLoading = true;
991 i.setState(this.state);
994 handleModBanFromCommunityShow(i: CommentNode) {
995 i.state.showBanDialog = !i.state.showBanDialog;
996 i.state.banType = BanType.Community;
1000 handleModBanShow(i: CommentNode) {
1001 i.state.showBanDialog = !i.state.showBanDialog;
1002 i.state.banType = BanType.Site;
1003 i.setState(i.state);
1006 handleModBanReasonChange(i: CommentNode, event: any) {
1007 i.state.banReason = event.target.value;
1008 i.setState(i.state);
1011 handleModBanExpiresChange(i: CommentNode, event: any) {
1012 i.state.banExpires = event.target.value;
1013 i.setState(i.state);
1016 handleModBanFromCommunitySubmit(i: CommentNode) {
1017 i.state.banType = BanType.Community;
1018 i.setState(i.state);
1019 i.handleModBanBothSubmit(i);
1022 handleModBanSubmit(i: CommentNode) {
1023 i.state.banType = BanType.Site;
1024 i.setState(i.state);
1025 i.handleModBanBothSubmit(i);
1028 handleModBanBothSubmit(i: CommentNode) {
1029 event.preventDefault();
1031 if (i.state.banType == BanType.Community) {
1032 let form: BanFromCommunityForm = {
1033 user_id: i.props.node.comment.creator_id,
1034 community_id: i.props.node.comment.community_id,
1035 ban: !i.props.node.comment.banned_from_community,
1036 reason: i.state.banReason,
1037 expires: getUnixTime(i.state.banExpires),
1039 WebSocketService.Instance.banFromCommunity(form);
1041 let form: BanUserForm = {
1042 user_id: i.props.node.comment.creator_id,
1043 ban: !i.props.node.comment.banned,
1044 reason: i.state.banReason,
1045 expires: getUnixTime(i.state.banExpires),
1047 WebSocketService.Instance.banUser(form);
1050 i.state.showBanDialog = false;
1051 i.setState(i.state);
1054 handleShowConfirmAppointAsMod(i: CommentNode) {
1055 i.state.showConfirmAppointAsMod = true;
1056 i.setState(i.state);
1059 handleCancelConfirmAppointAsMod(i: CommentNode) {
1060 i.state.showConfirmAppointAsMod = false;
1061 i.setState(i.state);
1064 handleAddModToCommunity(i: CommentNode) {
1065 let form: AddModToCommunityForm = {
1066 user_id: i.props.node.comment.creator_id,
1067 community_id: i.props.node.comment.community_id,
1070 WebSocketService.Instance.addModToCommunity(form);
1071 i.state.showConfirmAppointAsMod = false;
1072 i.setState(i.state);
1075 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1076 i.state.showConfirmAppointAsAdmin = true;
1077 i.setState(i.state);
1080 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1081 i.state.showConfirmAppointAsAdmin = false;
1082 i.setState(i.state);
1085 handleAddAdmin(i: CommentNode) {
1086 let form: AddAdminForm = {
1087 user_id: i.props.node.comment.creator_id,
1090 WebSocketService.Instance.addAdmin(form);
1091 i.state.showConfirmAppointAsAdmin = false;
1092 i.setState(i.state);
1095 handleShowConfirmTransferCommunity(i: CommentNode) {
1096 i.state.showConfirmTransferCommunity = true;
1097 i.setState(i.state);
1100 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1101 i.state.showConfirmTransferCommunity = false;
1102 i.setState(i.state);
1105 handleTransferCommunity(i: CommentNode) {
1106 let form: TransferCommunityForm = {
1107 community_id: i.props.node.comment.community_id,
1108 user_id: i.props.node.comment.creator_id,
1110 WebSocketService.Instance.transferCommunity(form);
1111 i.state.showConfirmTransferCommunity = false;
1112 i.setState(i.state);
1115 handleShowConfirmTransferSite(i: CommentNode) {
1116 i.state.showConfirmTransferSite = true;
1117 i.setState(i.state);
1120 handleCancelShowConfirmTransferSite(i: CommentNode) {
1121 i.state.showConfirmTransferSite = false;
1122 i.setState(i.state);
1125 handleTransferSite(i: CommentNode) {
1126 let form: TransferSiteForm = {
1127 user_id: i.props.node.comment.creator_id,
1129 WebSocketService.Instance.transferSite(form);
1130 i.state.showConfirmTransferSite = false;
1131 i.setState(i.state);
1134 get isCommentNew(): boolean {
1135 let now = moment.utc().subtract(10, 'minutes');
1136 let then = moment.utc(this.props.node.comment.published);
1137 return now.isBefore(then);
1140 handleCommentCollapse(i: CommentNode) {
1141 i.state.collapsed = !i.state.collapsed;
1142 i.setState(i.state);
1145 handleViewSource(i: CommentNode) {
1146 i.state.viewSource = !i.state.viewSource;
1147 i.setState(i.state);
1150 handleShowAdvanced(i: CommentNode) {
1151 i.state.showAdvanced = !i.state.showAdvanced;
1152 i.setState(i.state);
1157 if (this.state.my_vote == 1) {
1159 } else if (this.state.my_vote == -1) {
1160 return 'text-danger';
1162 return 'text-muted';
1166 get pointsTippy(): string {
1167 let points = i18n.t('number_of_points', {
1168 count: this.state.score,
1171 let upvotes = i18n.t('number_of_upvotes', {
1172 count: this.state.upvotes,
1175 let downvotes = i18n.t('number_of_downvotes', {
1176 count: this.state.downvotes,
1179 return `${points} • ${upvotes} • ${downvotes}`;