import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { CreateCommentLike, DeleteComment, RemoveComment, MarkCommentAsRead, MarkUserMentionAsRead, SaveComment, BanFromCommunity, BanUser, CommunityModeratorView, UserViewSafe, AddModToCommunity, AddAdmin, TransferCommunity, TransferSite, SortType, CommentView, UserMentionView, } from 'lemmy-js-client'; import { CommentSortType, CommentNode as CommentNodeI, BanType, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { mdToHtml, getUnixTime, canMod, isMod, setupTippy, colorList, wsClient, authField, } from '../utils'; import moment from 'moment'; import { MomentTime } from './moment-time'; import { CommentForm } from './comment-form'; import { CommentNodes } from './comment-nodes'; import { UserListing } from './user-listing'; import { CommunityLink } from './community-link'; import { i18n } from '../i18next'; interface CommentNodeState { showReply: boolean; showEdit: boolean; showRemoveDialog: boolean; removeReason: string; showBanDialog: boolean; removeData: boolean; banReason: string; banExpires: string; banType: BanType; showConfirmTransferSite: boolean; showConfirmTransferCommunity: boolean; showConfirmAppointAsMod: boolean; showConfirmAppointAsAdmin: boolean; collapsed: boolean; viewSource: boolean; showAdvanced: boolean; my_vote: number; score: number; upvotes: number; downvotes: number; borderColor: string; readLoading: boolean; saveLoading: boolean; } interface CommentNodeProps { node: CommentNodeI; noBorder?: boolean; noIndent?: boolean; viewOnly?: boolean; locked?: boolean; markable?: boolean; showContext?: boolean; moderators: CommunityModeratorView[]; admins: UserViewSafe[]; // TODO is this necessary, can't I get it from the node itself? postCreatorId?: number; showCommunity?: boolean; sort?: CommentSortType; sortType?: SortType; enableDownvotes: boolean; } export class CommentNode extends Component { private emptyState: CommentNodeState = { showReply: false, showEdit: false, showRemoveDialog: false, removeReason: null, showBanDialog: false, removeData: null, banReason: null, banExpires: null, banType: BanType.Community, collapsed: false, viewSource: false, showAdvanced: false, showConfirmTransferSite: false, showConfirmTransferCommunity: false, showConfirmAppointAsMod: false, showConfirmAppointAsAdmin: false, my_vote: this.props.node.comment_view.my_vote, score: this.props.node.comment_view.counts.score, upvotes: this.props.node.comment_view.counts.upvotes, downvotes: this.props.node.comment_view.counts.downvotes, borderColor: this.props.node.depth ? colorList[this.props.node.depth % colorList.length] : colorList[0], readLoading: false, saveLoading: false, }; constructor(props: any, context: any) { super(props, context); this.state = this.emptyState; this.handleReplyCancel = this.handleReplyCancel.bind(this); this.handleCommentUpvote = this.handleCommentUpvote.bind(this); this.handleCommentDownvote = this.handleCommentDownvote.bind(this); } // TODO see if there's a better way to do this, and all willReceiveProps componentWillReceiveProps(nextProps: CommentNodeProps) { let cv = nextProps.node.comment_view; this.state.my_vote = cv.my_vote; this.state.upvotes = cv.counts.upvotes; this.state.downvotes = cv.counts.downvotes; this.state.score = cv.counts.score; this.state.readLoading = false; this.state.saveLoading = false; this.setState(this.state); } render() { let node = this.props.node; let cv = this.props.node.comment_view; return (
{this.isMod && (
{i18n.t('mod')}
)} {this.isAdmin && (
{i18n.t('admin')}
)} {this.isPostCreator && (
{i18n.t('creator')}
)} {(cv.creator_banned_from_community || cv.creator.banned) && (
{i18n.t('banned')}
)} {this.props.showCommunity && ( <> {i18n.t('to')} {cv.post.name} )} {/* This is an expanding spacer for mobile */}
{this.state.score}
{/* end of user row */} {this.state.showEdit && ( )} {!this.state.showEdit && !this.state.collapsed && (
{this.state.viewSource ? (
{this.commentUnlessRemoved}
) : (
)}
{this.props.showContext && this.linkBtn} {this.props.markable && ( )} {UserService.Instance.user && !this.props.viewOnly && ( <> {this.props.enableDownvotes && ( )} {!this.state.showAdvanced ? ( ) : ( <> {!this.myComment && ( )} {!this.props.showContext && this.linkBtn} {this.myComment && ( <> )} {/* Admins and mods can remove comments */} {(this.canMod || this.canAdmin) && ( <> {!cv.comment.removed ? ( ) : ( )} )} {/* Mods can ban from community, and appoint as mods to community */} {this.canMod && ( <> {!this.isMod && (!cv.creator_banned_from_community ? ( ) : ( ))} {!cv.creator_banned_from_community && cv.creator.local && (!this.state.showConfirmAppointAsMod ? ( ) : ( <> ))} )} {/* Community creators and admins can transfer community to another mod */} {(this.amCommunityCreator || this.canAdmin) && this.isMod && cv.creator.local && (!this.state.showConfirmTransferCommunity ? ( ) : ( <> ))} {/* Admins can ban from all, and appoint other admins */} {this.canAdmin && ( <> {!this.isAdmin && (!cv.creator.banned ? ( ) : ( ))} {!cv.creator.banned && cv.creator.local && (!this.state.showConfirmAppointAsAdmin ? ( ) : ( <> ))} )} {/* Site Creator can transfer to another admin */} {this.amSiteCreator && this.isAdmin && cv.creator.local && (!this.state.showConfirmTransferSite ? ( ) : ( <> ))} )} )}
{/* end of button group */}
)}
{/* end of details */} {this.state.showRemoveDialog && (
)} {this.state.showBanDialog && (
{/* TODO hold off on expires until later */} {/*
*/} {/* */} {/* */} {/*
*/}
)} {this.state.showReply && ( )} {node.children && !this.state.collapsed && ( )} {/* A collapsed clearfix */} {this.state.collapsed &&
}
); } get commentOrMentionRead() { let cv = this.props.node.comment_view; return this.isUserMentionType(cv) ? cv.user_mention.read : cv.comment.read; } get linkBtn() { let cv = this.props.node.comment_view; return ( ); } get loadingIcon() { return ( ); } get myComment(): boolean { return ( UserService.Instance.user && this.props.node.comment_view.creator.id == UserService.Instance.user.id ); } get isMod(): boolean { return ( this.props.moderators && isMod( this.props.moderators.map(m => m.moderator.id), this.props.node.comment_view.creator.id ) ); } get isAdmin(): boolean { return ( this.props.admins && isMod( this.props.admins.map(a => a.user.id), this.props.node.comment_view.creator.id ) ); } get isPostCreator(): boolean { return this.props.node.comment_view.creator.id == this.props.postCreatorId; } get canMod(): boolean { if (this.props.admins && this.props.moderators) { let adminsThenMods = this.props.admins .map(a => a.user.id) .concat(this.props.moderators.map(m => m.moderator.id)); return canMod( UserService.Instance.user, adminsThenMods, this.props.node.comment_view.creator.id ); } else { return false; } } get canAdmin(): boolean { return ( this.props.admins && canMod( UserService.Instance.user, this.props.admins.map(a => a.user.id), this.props.node.comment_view.creator.id ) ); } get amCommunityCreator(): boolean { return ( this.props.moderators && UserService.Instance.user && this.props.node.comment_view.creator.id != UserService.Instance.user.id && UserService.Instance.user.id == this.props.moderators[0].moderator.id ); } get amSiteCreator(): boolean { return ( this.props.admins && UserService.Instance.user && this.props.node.comment_view.creator.id != UserService.Instance.user.id && UserService.Instance.user.id == this.props.admins[0].user.id ); } get commentUnlessRemoved(): string { let comment = this.props.node.comment_view.comment; return comment.removed ? `*${i18n.t('removed')}*` : comment.deleted ? `*${i18n.t('deleted')}*` : comment.content; } handleReplyClick(i: CommentNode) { i.state.showReply = true; i.setState(i.state); } handleEditClick(i: CommentNode) { i.state.showEdit = true; i.setState(i.state); } handleDeleteClick(i: CommentNode) { let comment = i.props.node.comment_view.comment; let deleteForm: DeleteComment = { comment_id: comment.id, deleted: !comment.deleted, auth: authField(), }; WebSocketService.Instance.send(wsClient.deleteComment(deleteForm)); } handleSaveCommentClick(i: CommentNode) { let cv = i.props.node.comment_view; let save = cv.saved == undefined ? true : !cv.saved; let form: SaveComment = { comment_id: cv.comment.id, save, auth: authField(), }; WebSocketService.Instance.send(wsClient.saveComment(form)); i.state.saveLoading = true; i.setState(this.state); } handleReplyCancel() { this.state.showReply = false; this.state.showEdit = false; this.setState(this.state); } handleCommentUpvote(i: CommentNodeI) { let new_vote = this.state.my_vote == 1 ? 0 : 1; if (this.state.my_vote == 1) { this.state.score--; this.state.upvotes--; } else if (this.state.my_vote == -1) { this.state.downvotes--; this.state.upvotes++; this.state.score += 2; } else { this.state.upvotes++; this.state.score++; } this.state.my_vote = new_vote; let form: CreateCommentLike = { comment_id: i.comment_view.comment.id, score: this.state.my_vote, auth: authField(), }; WebSocketService.Instance.send(wsClient.likeComment(form)); this.setState(this.state); setupTippy(); } handleCommentDownvote(i: CommentNodeI) { let new_vote = this.state.my_vote == -1 ? 0 : -1; if (this.state.my_vote == 1) { this.state.score -= 2; this.state.upvotes--; this.state.downvotes++; } else if (this.state.my_vote == -1) { this.state.downvotes--; this.state.score++; } else { this.state.downvotes++; this.state.score--; } this.state.my_vote = new_vote; let form: CreateCommentLike = { comment_id: i.comment_view.comment.id, score: this.state.my_vote, auth: authField(), }; WebSocketService.Instance.send(wsClient.likeComment(form)); this.setState(this.state); setupTippy(); } handleModRemoveShow(i: CommentNode) { i.state.showRemoveDialog = true; i.setState(i.state); } handleModRemoveReasonChange(i: CommentNode, event: any) { i.state.removeReason = event.target.value; i.setState(i.state); } handleModRemoveDataChange(i: CommentNode, event: any) { i.state.removeData = event.target.checked; i.setState(i.state); } handleModRemoveSubmit(i: CommentNode) { let comment = i.props.node.comment_view.comment; let form: RemoveComment = { comment_id: comment.id, removed: !comment.removed, reason: i.state.removeReason, auth: authField(), }; WebSocketService.Instance.send(wsClient.removeComment(form)); i.state.showRemoveDialog = false; i.setState(i.state); } isUserMentionType( item: CommentView | UserMentionView ): item is UserMentionView { return (item as UserMentionView).user_mention?.id !== undefined; } handleMarkRead(i: CommentNode) { if (i.isUserMentionType(i.props.node.comment_view)) { let form: MarkUserMentionAsRead = { user_mention_id: i.props.node.comment_view.user_mention.id, read: !i.props.node.comment_view.user_mention.read, auth: authField(), }; WebSocketService.Instance.send(wsClient.markUserMentionAsRead(form)); } else { let form: MarkCommentAsRead = { comment_id: i.props.node.comment_view.comment.id, read: !i.props.node.comment_view.comment.read, auth: authField(), }; WebSocketService.Instance.send(wsClient.markCommentAsRead(form)); } i.state.readLoading = true; i.setState(this.state); } handleModBanFromCommunityShow(i: CommentNode) { i.state.showBanDialog = !i.state.showBanDialog; i.state.banType = BanType.Community; i.setState(i.state); } handleModBanShow(i: CommentNode) { i.state.showBanDialog = !i.state.showBanDialog; i.state.banType = BanType.Site; i.setState(i.state); } handleModBanReasonChange(i: CommentNode, event: any) { i.state.banReason = event.target.value; i.setState(i.state); } handleModBanExpiresChange(i: CommentNode, event: any) { i.state.banExpires = event.target.value; i.setState(i.state); } handleModBanFromCommunitySubmit(i: CommentNode) { i.state.banType = BanType.Community; i.setState(i.state); i.handleModBanBothSubmit(i); } handleModBanSubmit(i: CommentNode) { i.state.banType = BanType.Site; i.setState(i.state); i.handleModBanBothSubmit(i); } handleModBanBothSubmit(i: CommentNode) { let cv = i.props.node.comment_view; if (i.state.banType == BanType.Community) { // If its an unban, restore all their data let ban = !cv.creator_banned_from_community; if (ban == false) { i.state.removeData = false; } let form: BanFromCommunity = { user_id: cv.creator.id, community_id: cv.community.id, ban, remove_data: i.state.removeData, reason: i.state.banReason, expires: getUnixTime(i.state.banExpires), auth: authField(), }; WebSocketService.Instance.send(wsClient.banFromCommunity(form)); } else { // If its an unban, restore all their data let ban = !cv.creator.banned; if (ban == false) { i.state.removeData = false; } let form: BanUser = { user_id: cv.creator.id, ban, remove_data: i.state.removeData, reason: i.state.banReason, expires: getUnixTime(i.state.banExpires), auth: authField(), }; WebSocketService.Instance.send(wsClient.banUser(form)); } i.state.showBanDialog = false; i.setState(i.state); } handleShowConfirmAppointAsMod(i: CommentNode) { i.state.showConfirmAppointAsMod = true; i.setState(i.state); } handleCancelConfirmAppointAsMod(i: CommentNode) { i.state.showConfirmAppointAsMod = false; i.setState(i.state); } handleAddModToCommunity(i: CommentNode) { let cv = i.props.node.comment_view; let form: AddModToCommunity = { user_id: cv.creator.id, community_id: cv.community.id, added: !i.isMod, auth: authField(), }; WebSocketService.Instance.send(wsClient.addModToCommunity(form)); i.state.showConfirmAppointAsMod = false; i.setState(i.state); } handleShowConfirmAppointAsAdmin(i: CommentNode) { i.state.showConfirmAppointAsAdmin = true; i.setState(i.state); } handleCancelConfirmAppointAsAdmin(i: CommentNode) { i.state.showConfirmAppointAsAdmin = false; i.setState(i.state); } handleAddAdmin(i: CommentNode) { let form: AddAdmin = { user_id: i.props.node.comment_view.creator.id, added: !i.isAdmin, auth: authField(), }; WebSocketService.Instance.send(wsClient.addAdmin(form)); i.state.showConfirmAppointAsAdmin = false; i.setState(i.state); } handleShowConfirmTransferCommunity(i: CommentNode) { i.state.showConfirmTransferCommunity = true; i.setState(i.state); } handleCancelShowConfirmTransferCommunity(i: CommentNode) { i.state.showConfirmTransferCommunity = false; i.setState(i.state); } handleTransferCommunity(i: CommentNode) { let cv = i.props.node.comment_view; let form: TransferCommunity = { community_id: cv.community.id, user_id: cv.creator.id, auth: authField(), }; WebSocketService.Instance.send(wsClient.transferCommunity(form)); i.state.showConfirmTransferCommunity = false; i.setState(i.state); } handleShowConfirmTransferSite(i: CommentNode) { i.state.showConfirmTransferSite = true; i.setState(i.state); } handleCancelShowConfirmTransferSite(i: CommentNode) { i.state.showConfirmTransferSite = false; i.setState(i.state); } handleTransferSite(i: CommentNode) { let form: TransferSite = { user_id: i.props.node.comment_view.creator.id, auth: authField(), }; WebSocketService.Instance.send(wsClient.transferSite(form)); i.state.showConfirmTransferSite = false; i.setState(i.state); } get isCommentNew(): boolean { let now = moment.utc().subtract(10, 'minutes'); let then = moment.utc(this.props.node.comment_view.comment.published); return now.isBefore(then); } handleCommentCollapse(i: CommentNode) { i.state.collapsed = !i.state.collapsed; i.setState(i.state); } handleViewSource(i: CommentNode) { i.state.viewSource = !i.state.viewSource; i.setState(i.state); } handleShowAdvanced(i: CommentNode) { i.state.showAdvanced = !i.state.showAdvanced; i.setState(i.state); setupTippy(); } get scoreColor() { if (this.state.my_vote == 1) { return 'text-info'; } else if (this.state.my_vote == -1) { return 'text-danger'; } else { return 'text-muted'; } } get pointsTippy(): string { let points = i18n.t('number_of_points', { count: this.state.score, }); let upvotes = i18n.t('number_of_upvotes', { count: this.state.upvotes, }); let downvotes = i18n.t('number_of_downvotes', { count: this.state.downvotes, }); return `${points} • ${upvotes} • ${downvotes}`; } }