1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
12 CommunityModeratorView,
21 } from 'lemmy-js-client';
24 CommentNode as CommentNodeI,
26 } from '../interfaces';
27 import { WebSocketService, UserService } from '../services';
38 import moment from 'moment';
39 import { MomentTime } from './moment-time';
40 import { CommentForm } from './comment-form';
41 import { CommentNodes } from './comment-nodes';
42 import { UserListing } from './user-listing';
43 import { CommunityLink } from './community-link';
44 import { i18n } from '../i18next';
46 interface CommentNodeState {
49 showRemoveDialog: boolean;
51 showBanDialog: boolean;
56 showConfirmTransferSite: boolean;
57 showConfirmTransferCommunity: boolean;
58 showConfirmAppointAsMod: boolean;
59 showConfirmAppointAsAdmin: boolean;
62 showAdvanced: boolean;
72 interface CommentNodeProps {
79 showContext?: boolean;
80 moderators: CommunityModeratorView[];
81 admins: UserViewSafe[];
82 // TODO is this necessary, can't I get it from the node itself?
83 postCreatorId?: number;
84 showCommunity?: boolean;
85 sort?: CommentSortType;
87 enableDownvotes: boolean;
90 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
91 private emptyState: CommentNodeState = {
94 showRemoveDialog: false,
100 banType: BanType.Community,
104 showConfirmTransferSite: false,
105 showConfirmTransferCommunity: false,
106 showConfirmAppointAsMod: false,
107 showConfirmAppointAsAdmin: false,
108 my_vote: this.props.node.comment_view.my_vote,
109 score: this.props.node.comment_view.counts.score,
110 upvotes: this.props.node.comment_view.counts.upvotes,
111 downvotes: this.props.node.comment_view.counts.downvotes,
112 borderColor: this.props.node.depth
113 ? colorList[this.props.node.depth % colorList.length]
119 constructor(props: any, context: any) {
120 super(props, context);
122 this.state = this.emptyState;
123 this.handleReplyCancel = this.handleReplyCancel.bind(this);
124 this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
125 this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
128 // TODO see if there's a better way to do this, and all willReceiveProps
129 componentWillReceiveProps(nextProps: CommentNodeProps) {
130 let cv = nextProps.node.comment_view;
131 this.state.my_vote = cv.my_vote;
132 this.state.upvotes = cv.counts.upvotes;
133 this.state.downvotes = cv.counts.downvotes;
134 this.state.score = cv.counts.score;
135 this.state.readLoading = false;
136 this.state.saveLoading = false;
137 this.setState(this.state);
141 let node = this.props.node;
142 let cv = this.props.node.comment_view;
145 className={`comment ${
146 cv.comment.parent_id && !this.props.noIndent ? 'ml-1' : ''
150 id={`comment-${cv.comment.id}`}
151 className={`details comment-node py-2 ${
152 !this.props.noBorder ? 'border-top border-light' : ''
153 } ${this.isCommentNew ? 'mark' : ''}`}
155 !this.props.noIndent &&
156 cv.comment.parent_id &&
157 `border-left: 2px ${this.state.borderColor} solid !important`
161 class={`${!this.props.noIndent && cv.comment.parent_id && 'ml-2'}`}
163 <div class="d-flex flex-wrap align-items-center text-muted small">
165 <UserListing user={cv.creator} />
169 <div className="badge badge-light d-none d-sm-inline mr-2">
174 <div className="badge badge-light d-none d-sm-inline mr-2">
178 {this.isPostCreator && (
179 <div className="badge badge-light d-none d-sm-inline mr-2">
183 {(cv.creator_banned_from_community || cv.creator.banned) && (
184 <div className="badge badge-danger mr-2">
188 {this.props.showCommunity && (
190 <span class="mx-1">{i18n.t('to')}</span>
191 <CommunityLink community={cv.community} />
192 <span class="mx-2">•</span>
193 <Link className="mr-2" to={`/post/${cv.post.id}`}>
199 class="btn btn-sm text-muted"
200 onClick={linkEvent(this, this.handleCommentCollapse)}
202 {this.state.collapsed ? '+' : '—'}
204 {/* This is an expanding spacer for mobile */}
205 <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
207 className={`unselectable pointer ${this.scoreColor}`}
208 onClick={linkEvent(node, this.handleCommentUpvote)}
209 data-tippy-content={this.pointsTippy}
211 <span class="mr-1 font-weight-bold">{this.state.score}</span>
213 <span className="mr-1">•</span>
215 <MomentTime data={cv.comment} />
218 {/* end of user row */}
219 {this.state.showEdit && (
223 onReplyCancel={this.handleReplyCancel}
224 disabled={this.props.locked}
228 {!this.state.showEdit && !this.state.collapsed && (
230 {this.state.viewSource ? (
231 <pre>{this.commentUnlessRemoved}</pre>
235 dangerouslySetInnerHTML={mdToHtml(
236 this.commentUnlessRemoved
240 <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
241 {this.props.showContext && this.linkBtn}
242 {this.props.markable && (
244 class="btn btn-link btn-animate text-muted"
245 onClick={linkEvent(this, this.handleMarkRead)}
247 this.commentOrMentionRead
248 ? i18n.t('mark_as_unread')
249 : i18n.t('mark_as_read')
252 {this.state.readLoading ? (
256 class={`icon icon-inline ${
257 this.commentOrMentionRead && 'text-success'
260 <use xlinkHref="#icon-check"></use>
265 {UserService.Instance.user && !this.props.viewOnly && (
268 className={`btn btn-link btn-animate ${
269 this.state.my_vote == 1 ? 'text-info' : 'text-muted'
271 onClick={linkEvent(node, this.handleCommentUpvote)}
272 data-tippy-content={i18n.t('upvote')}
274 <svg class="icon icon-inline">
275 <use xlinkHref="#icon-arrow-up1"></use>
277 {this.state.upvotes !== this.state.score && (
278 <span class="ml-1">{this.state.upvotes}</span>
281 {this.props.enableDownvotes && (
283 className={`btn btn-link btn-animate ${
284 this.state.my_vote == -1
288 onClick={linkEvent(node, this.handleCommentDownvote)}
289 data-tippy-content={i18n.t('downvote')}
291 <svg class="icon icon-inline">
292 <use xlinkHref="#icon-arrow-down1"></use>
294 {this.state.upvotes !== this.state.score && (
295 <span class="ml-1">{this.state.downvotes}</span>
300 class="btn btn-link btn-animate text-muted"
301 onClick={linkEvent(this, this.handleReplyClick)}
302 data-tippy-content={i18n.t('reply')}
304 <svg class="icon icon-inline">
305 <use xlinkHref="#icon-reply1"></use>
308 {!this.state.showAdvanced ? (
310 className="btn btn-link btn-animate text-muted"
311 onClick={linkEvent(this, this.handleShowAdvanced)}
312 data-tippy-content={i18n.t('more')}
314 <svg class="icon icon-inline">
315 <use xlinkHref="#icon-more-vertical"></use>
320 {!this.myComment && (
321 <button class="btn btn-link btn-animate">
323 className="text-muted"
324 to={`/create_private_message/recipient/${cv.creator.id}`}
325 title={i18n.t('message').toLowerCase()}
328 <use xlinkHref="#icon-mail"></use>
333 {!this.props.showContext && this.linkBtn}
335 class="btn btn-link btn-animate text-muted"
338 this.handleSaveCommentClick
341 cv.saved ? i18n.t('unsave') : i18n.t('save')
344 {this.state.saveLoading ? (
348 class={`icon icon-inline ${
349 cv.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
393 class={`icon icon-inline ${
394 cv.comment.deleted && 'text-danger'
397 <use xlinkHref="#icon-trash"></use>
402 {/* Admins and mods can remove comments */}
403 {(this.canMod || this.canAdmin) && (
405 {!cv.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 (!cv.creator_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 {!cv.creator_banned_from_community &&
455 (!this.state.showConfirmAppointAsMod ? (
457 class="btn btn-link btn-animate text-muted"
460 this.handleShowConfirmAppointAsMod
464 ? i18n.t('remove_as_mod')
465 : i18n.t('appoint_as_mod')}
469 <button class="btn btn-link btn-animate text-muted">
470 {i18n.t('are_you_sure')}
473 class="btn btn-link btn-animate text-muted"
476 this.handleAddModToCommunity
482 class="btn btn-link btn-animate text-muted"
485 this.handleCancelConfirmAppointAsMod
494 {/* Community creators and admins can transfer community to another mod */}
495 {(this.amCommunityCreator || this.canAdmin) &&
498 (!this.state.showConfirmTransferCommunity ? (
500 class="btn btn-link btn-animate text-muted"
503 this.handleShowConfirmTransferCommunity
506 {i18n.t('transfer_community')}
510 <button class="btn btn-link btn-animate text-muted">
511 {i18n.t('are_you_sure')}
514 class="btn btn-link btn-animate text-muted"
517 this.handleTransferCommunity
523 class="btn btn-link btn-animate text-muted"
527 .handleCancelShowConfirmTransferCommunity
534 {/* Admins can ban from all, and appoint other admins */}
538 (!cv.creator.banned ? (
540 class="btn btn-link btn-animate text-muted"
543 this.handleModBanShow
546 {i18n.t('ban_from_site')}
550 class="btn btn-link btn-animate text-muted"
553 this.handleModBanSubmit
556 {i18n.t('unban_from_site')}
559 {!cv.creator.banned &&
561 (!this.state.showConfirmAppointAsAdmin ? (
563 class="btn btn-link btn-animate text-muted"
566 this.handleShowConfirmAppointAsAdmin
570 ? i18n.t('remove_as_admin')
571 : i18n.t('appoint_as_admin')}
575 <button class="btn btn-link btn-animate text-muted">
576 {i18n.t('are_you_sure')}
579 class="btn btn-link btn-animate text-muted"
588 class="btn btn-link btn-animate text-muted"
591 this.handleCancelConfirmAppointAsAdmin
600 {/* Site Creator can transfer to another admin */}
601 {this.amSiteCreator &&
604 (!this.state.showConfirmTransferSite ? (
606 class="btn btn-link btn-animate text-muted"
609 this.handleShowConfirmTransferSite
612 {i18n.t('transfer_site')}
616 <button class="btn btn-link btn-animate text-muted">
617 {i18n.t('are_you_sure')}
620 class="btn btn-link btn-animate text-muted"
623 this.handleTransferSite
629 class="btn btn-link btn-animate text-muted"
632 this.handleCancelShowConfirmTransferSite
644 {/* end of button group */}
649 {/* end of details */}
650 {this.state.showRemoveDialog && (
653 onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
657 class="form-control mr-2"
658 placeholder={i18n.t('reason')}
659 value={this.state.removeReason}
660 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
662 <button type="submit" class="btn btn-secondary">
663 {i18n.t('remove_comment')}
667 {this.state.showBanDialog && (
668 <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
669 <div class="form-group row">
670 <label class="col-form-label">{i18n.t('reason')}</label>
673 class="form-control mr-2"
674 placeholder={i18n.t('reason')}
675 value={this.state.banReason}
676 onInput={linkEvent(this, this.handleModBanReasonChange)}
678 <div class="form-group">
679 <div class="form-check">
681 class="form-check-input"
682 id="mod-ban-remove-data"
684 checked={this.state.removeData}
685 onChange={linkEvent(this, this.handleModRemoveDataChange)}
687 <label class="form-check-label" htmlFor="mod-ban-remove-data">
688 {i18n.t('remove_posts_comments')}
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')} {cv.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>}
731 get commentOrMentionRead() {
732 let cv = this.props.node.comment_view;
733 return this.isUserMentionType(cv) ? cv.user_mention.read : cv.comment.read;
737 let cv = this.props.node.comment_view;
740 className="btn btn-link btn-animate text-muted"
741 to={`/post/${cv.post.id}/comment/${cv.comment.id}`}
742 title={this.props.showContext ? i18n.t('show_context') : i18n.t('link')}
744 <svg class="icon icon-inline">
745 <use xlinkHref="#icon-link"></use>
753 <svg class="icon icon-spinner spin">
754 <use xlinkHref="#icon-spinner"></use>
759 get myComment(): boolean {
761 UserService.Instance.user &&
762 this.props.node.comment_view.creator.id == UserService.Instance.user.id
766 get isMod(): boolean {
768 this.props.moderators &&
770 this.props.moderators.map(m => m.moderator.id),
771 this.props.node.comment_view.creator.id
776 get isAdmin(): boolean {
780 this.props.admins.map(a => a.user.id),
781 this.props.node.comment_view.creator.id
786 get isPostCreator(): boolean {
787 return this.props.node.comment_view.creator.id == this.props.postCreatorId;
790 get canMod(): boolean {
791 if (this.props.admins && this.props.moderators) {
792 let adminsThenMods = this.props.admins
794 .concat(this.props.moderators.map(m => m.moderator.id));
797 UserService.Instance.user,
799 this.props.node.comment_view.creator.id
806 get canAdmin(): boolean {
810 UserService.Instance.user,
811 this.props.admins.map(a => a.user.id),
812 this.props.node.comment_view.creator.id
817 get amCommunityCreator(): boolean {
819 this.props.moderators &&
820 UserService.Instance.user &&
821 this.props.node.comment_view.creator.id != UserService.Instance.user.id &&
822 UserService.Instance.user.id == this.props.moderators[0].moderator.id
826 get amSiteCreator(): boolean {
829 UserService.Instance.user &&
830 this.props.node.comment_view.creator.id != UserService.Instance.user.id &&
831 UserService.Instance.user.id == this.props.admins[0].user.id
835 get commentUnlessRemoved(): string {
836 let comment = this.props.node.comment_view.comment;
837 return comment.removed
838 ? `*${i18n.t('removed')}*`
840 ? `*${i18n.t('deleted')}*`
844 handleReplyClick(i: CommentNode) {
845 i.state.showReply = true;
849 handleEditClick(i: CommentNode) {
850 i.state.showEdit = true;
854 handleDeleteClick(i: CommentNode) {
855 let comment = i.props.node.comment_view.comment;
856 let deleteForm: DeleteComment = {
857 comment_id: comment.id,
858 deleted: !comment.deleted,
861 WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
864 handleSaveCommentClick(i: CommentNode) {
865 let cv = i.props.node.comment_view;
866 let save = cv.saved == undefined ? true : !cv.saved;
867 let form: SaveComment = {
868 comment_id: cv.comment.id,
873 WebSocketService.Instance.send(wsClient.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: CreateCommentLike = {
903 comment_id: i.comment_view.comment.id,
904 score: this.state.my_vote,
908 WebSocketService.Instance.send(wsClient.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: CreateCommentLike = {
931 comment_id: i.comment_view.comment.id,
932 score: this.state.my_vote,
936 WebSocketService.Instance.send(wsClient.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 handleModRemoveDataChange(i: CommentNode, event: any) {
952 i.state.removeData = event.target.checked;
956 handleModRemoveSubmit(i: CommentNode) {
957 let comment = i.props.node.comment_view.comment;
958 let form: RemoveComment = {
959 comment_id: comment.id,
960 removed: !comment.removed,
961 reason: i.state.removeReason,
964 WebSocketService.Instance.send(wsClient.removeComment(form));
966 i.state.showRemoveDialog = false;
971 item: CommentView | UserMentionView
972 ): item is UserMentionView {
973 return (item as UserMentionView).user_mention?.id !== undefined;
976 handleMarkRead(i: CommentNode) {
977 if (i.isUserMentionType(i.props.node.comment_view)) {
978 let form: MarkUserMentionAsRead = {
979 user_mention_id: i.props.node.comment_view.user_mention.id,
980 read: !i.props.node.comment_view.user_mention.read,
983 WebSocketService.Instance.send(wsClient.markUserMentionAsRead(form));
985 let form: MarkCommentAsRead = {
986 comment_id: i.props.node.comment_view.comment.id,
987 read: !i.props.node.comment_view.comment.read,
990 WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
993 i.state.readLoading = true;
994 i.setState(this.state);
997 handleModBanFromCommunityShow(i: CommentNode) {
998 i.state.showBanDialog = !i.state.showBanDialog;
999 i.state.banType = BanType.Community;
1000 i.setState(i.state);
1003 handleModBanShow(i: CommentNode) {
1004 i.state.showBanDialog = !i.state.showBanDialog;
1005 i.state.banType = BanType.Site;
1006 i.setState(i.state);
1009 handleModBanReasonChange(i: CommentNode, event: any) {
1010 i.state.banReason = event.target.value;
1011 i.setState(i.state);
1014 handleModBanExpiresChange(i: CommentNode, event: any) {
1015 i.state.banExpires = event.target.value;
1016 i.setState(i.state);
1019 handleModBanFromCommunitySubmit(i: CommentNode) {
1020 i.state.banType = BanType.Community;
1021 i.setState(i.state);
1022 i.handleModBanBothSubmit(i);
1025 handleModBanSubmit(i: CommentNode) {
1026 i.state.banType = BanType.Site;
1027 i.setState(i.state);
1028 i.handleModBanBothSubmit(i);
1031 handleModBanBothSubmit(i: CommentNode) {
1032 let cv = i.props.node.comment_view;
1034 if (i.state.banType == BanType.Community) {
1035 // If its an unban, restore all their data
1036 let ban = !cv.creator_banned_from_community;
1038 i.state.removeData = false;
1040 let form: BanFromCommunity = {
1041 user_id: cv.creator.id,
1042 community_id: cv.community.id,
1044 remove_data: i.state.removeData,
1045 reason: i.state.banReason,
1046 expires: getUnixTime(i.state.banExpires),
1049 WebSocketService.Instance.send(wsClient.banFromCommunity(form));
1051 // If its an unban, restore all their data
1052 let ban = !cv.creator.banned;
1054 i.state.removeData = false;
1056 let form: BanUser = {
1057 user_id: cv.creator.id,
1059 remove_data: i.state.removeData,
1060 reason: i.state.banReason,
1061 expires: getUnixTime(i.state.banExpires),
1064 WebSocketService.Instance.send(wsClient.banUser(form));
1067 i.state.showBanDialog = false;
1068 i.setState(i.state);
1071 handleShowConfirmAppointAsMod(i: CommentNode) {
1072 i.state.showConfirmAppointAsMod = true;
1073 i.setState(i.state);
1076 handleCancelConfirmAppointAsMod(i: CommentNode) {
1077 i.state.showConfirmAppointAsMod = false;
1078 i.setState(i.state);
1081 handleAddModToCommunity(i: CommentNode) {
1082 let cv = i.props.node.comment_view;
1083 let form: AddModToCommunity = {
1084 user_id: cv.creator.id,
1085 community_id: cv.community.id,
1089 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
1090 i.state.showConfirmAppointAsMod = false;
1091 i.setState(i.state);
1094 handleShowConfirmAppointAsAdmin(i: CommentNode) {
1095 i.state.showConfirmAppointAsAdmin = true;
1096 i.setState(i.state);
1099 handleCancelConfirmAppointAsAdmin(i: CommentNode) {
1100 i.state.showConfirmAppointAsAdmin = false;
1101 i.setState(i.state);
1104 handleAddAdmin(i: CommentNode) {
1105 let form: AddAdmin = {
1106 user_id: i.props.node.comment_view.creator.id,
1110 WebSocketService.Instance.send(wsClient.addAdmin(form));
1111 i.state.showConfirmAppointAsAdmin = false;
1112 i.setState(i.state);
1115 handleShowConfirmTransferCommunity(i: CommentNode) {
1116 i.state.showConfirmTransferCommunity = true;
1117 i.setState(i.state);
1120 handleCancelShowConfirmTransferCommunity(i: CommentNode) {
1121 i.state.showConfirmTransferCommunity = false;
1122 i.setState(i.state);
1125 handleTransferCommunity(i: CommentNode) {
1126 let cv = i.props.node.comment_view;
1127 let form: TransferCommunity = {
1128 community_id: cv.community.id,
1129 user_id: cv.creator.id,
1132 WebSocketService.Instance.send(wsClient.transferCommunity(form));
1133 i.state.showConfirmTransferCommunity = false;
1134 i.setState(i.state);
1137 handleShowConfirmTransferSite(i: CommentNode) {
1138 i.state.showConfirmTransferSite = true;
1139 i.setState(i.state);
1142 handleCancelShowConfirmTransferSite(i: CommentNode) {
1143 i.state.showConfirmTransferSite = false;
1144 i.setState(i.state);
1147 handleTransferSite(i: CommentNode) {
1148 let form: TransferSite = {
1149 user_id: i.props.node.comment_view.creator.id,
1152 WebSocketService.Instance.send(wsClient.transferSite(form));
1153 i.state.showConfirmTransferSite = false;
1154 i.setState(i.state);
1157 get isCommentNew(): boolean {
1158 let now = moment.utc().subtract(10, 'minutes');
1159 let then = moment.utc(this.props.node.comment_view.comment.published);
1160 return now.isBefore(then);
1163 handleCommentCollapse(i: CommentNode) {
1164 i.state.collapsed = !i.state.collapsed;
1165 i.setState(i.state);
1168 handleViewSource(i: CommentNode) {
1169 i.state.viewSource = !i.state.viewSource;
1170 i.setState(i.state);
1173 handleShowAdvanced(i: CommentNode) {
1174 i.state.showAdvanced = !i.state.showAdvanced;
1175 i.setState(i.state);
1180 if (this.state.my_vote == 1) {
1182 } else if (this.state.my_vote == -1) {
1183 return 'text-danger';
1185 return 'text-muted';
1189 get pointsTippy(): string {
1190 let points = i18n.t('number_of_points', {
1191 count: this.state.score,
1194 let upvotes = i18n.t('number_of_upvotes', {
1195 count: this.state.upvotes,
1198 let downvotes = i18n.t('number_of_downvotes', {
1199 count: this.state.downvotes,
1202 return `${points} • ${upvotes} • ${downvotes}`;