766 lines
25 KiB
TypeScript
766 lines
25 KiB
TypeScript
import { h, JSX, Component, createRef, ComponentType } from 'preact';
|
|
import { FormattedMessage, IntlShape, defineMessages } from 'react-intl';
|
|
import b from 'bem-react-helper';
|
|
import clsx from 'clsx';
|
|
|
|
import { getHandleClickProps } from 'common/accessibility';
|
|
import { COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants';
|
|
|
|
import { StaticStore } from 'common/static-store';
|
|
import { debounce } from 'utils/debounce';
|
|
import { copy } from 'common/copy';
|
|
import { Theme, BlockTTL, Comment as CommentType, PostInfo, User, CommentMode, Profile } from 'common/types';
|
|
import { isUserAnonymous } from 'utils/isUserAnonymous';
|
|
|
|
import { CommentFormProps } from 'components/comment-form';
|
|
import { Avatar } from 'components/avatar';
|
|
import { Button } from 'components/button';
|
|
import { Countdown } from 'components/countdown';
|
|
import { VerificationIcon } from 'components/icons/verification';
|
|
import { getPreview, uploadImage } from 'common/api';
|
|
import { postMessageToParent } from 'utils/post-message';
|
|
import { getBlockingDurations } from './getBlockingDurations';
|
|
import { boundActions } from './connected-comment';
|
|
import { CommentVotes } from './comment-votes';
|
|
|
|
import styles from './comment.module.css';
|
|
import './styles';
|
|
|
|
export type CommentProps = {
|
|
user: User | null;
|
|
CommentForm?: ComponentType<CommentFormProps>;
|
|
data: CommentType;
|
|
repliesCount?: number;
|
|
post_info?: PostInfo;
|
|
/** whether comment's user is banned */
|
|
isUserBanned?: boolean;
|
|
isCommentsDisabled: boolean;
|
|
/** edit mode: is comment should have reply, or edit Input */
|
|
editMode?: CommentMode;
|
|
/**
|
|
* "main" view used in main case,
|
|
* "pinned" view used in pinned block,
|
|
* "user" is for user comments widget,
|
|
* "preview" is for last comments page
|
|
*/
|
|
view: 'main' | 'pinned' | 'user' | 'preview';
|
|
/** defines whether comment should have reply/edit actions */
|
|
disabled?: boolean;
|
|
collapsed?: boolean;
|
|
theme: Theme;
|
|
inView?: boolean;
|
|
level?: number;
|
|
mix?: string;
|
|
getPreview?: typeof getPreview;
|
|
uploadImage?: typeof uploadImage;
|
|
intl: IntlShape;
|
|
} & Partial<typeof boundActions>;
|
|
|
|
export interface State {
|
|
renderDummy: boolean;
|
|
isCopied: boolean;
|
|
editDeadline: Date | null;
|
|
initial: boolean;
|
|
}
|
|
|
|
export class Comment extends Component<CommentProps, State> {
|
|
votingPromise: Promise<unknown> = Promise.resolve();
|
|
/** comment text node. Used in comment text copying */
|
|
textNode = createRef<HTMLDivElement>();
|
|
|
|
updateState = (props: CommentProps) => {
|
|
const newState: Partial<State> = {};
|
|
|
|
if (props.inView) {
|
|
newState.renderDummy = false;
|
|
}
|
|
|
|
// set comment edit timer
|
|
if (props.user && props.user.id === props.data.user.id) {
|
|
const editDuration = StaticStore.config.edit_duration;
|
|
const timeDiff = StaticStore.serverClientTimeDiff || 0;
|
|
const editDeadline = new Date(new Date(props.data.time).getTime() + timeDiff + editDuration * 1000);
|
|
|
|
if (editDeadline < new Date()) {
|
|
newState.editDeadline = null;
|
|
} else {
|
|
newState.editDeadline = editDeadline;
|
|
}
|
|
}
|
|
|
|
return newState;
|
|
};
|
|
|
|
state = {
|
|
renderDummy: typeof this.props.inView === 'boolean' ? !this.props.inView : false,
|
|
isCopied: false,
|
|
editDeadline: null,
|
|
voteErrorMessage: null,
|
|
initial: true,
|
|
...this.updateState(this.props),
|
|
};
|
|
|
|
// getHandleClickProps = (handler?: (e: KeyboardEvent | MouseEvent) => void) => {
|
|
// if (this.state.initial) return null;
|
|
// if (this.props.inView === false) return null;
|
|
// return getHandleClickProps(handler);
|
|
// };
|
|
|
|
componentWillReceiveProps(nextProps: CommentProps) {
|
|
this.setState(this.updateState(nextProps));
|
|
}
|
|
|
|
componentDidMount() {
|
|
// eslint-disable-next-line react/no-did-mount-set-state
|
|
this.setState({ initial: false });
|
|
}
|
|
|
|
toggleReplying = () => {
|
|
const { editMode, setReplyEditState, data } = this.props;
|
|
|
|
setReplyEditState?.({ id: data.id, state: editMode === CommentMode.Reply ? CommentMode.None : CommentMode.Reply });
|
|
};
|
|
|
|
toggleEditing = () => {
|
|
const { editMode, setReplyEditState, data } = this.props;
|
|
|
|
setReplyEditState?.({ id: data.id, state: editMode === CommentMode.Edit ? CommentMode.None : CommentMode.Edit });
|
|
};
|
|
|
|
toggleUserInfoVisibility = () => {
|
|
const profile: Profile = { ...this.props.data.user };
|
|
|
|
if (this.props.user?.id === profile.id) {
|
|
profile.current = '1';
|
|
}
|
|
|
|
postMessageToParent({ profile });
|
|
};
|
|
|
|
togglePin = () => {
|
|
const value = !this.props.data.pin;
|
|
const intl = this.props.intl;
|
|
const promptMessage = value ? intl.formatMessage(messages.pinComment) : intl.formatMessage(messages.unpinComment);
|
|
|
|
if (window.confirm(promptMessage)) {
|
|
this.props.setPinState!(this.props.data.id, value);
|
|
}
|
|
};
|
|
|
|
toggleVerify = () => {
|
|
const value = !this.props.data.user.verified;
|
|
const userId = this.props.data.user.id;
|
|
const intl = this.props.intl;
|
|
const userName = this.props.data.user.name;
|
|
const promptMessage = value
|
|
? intl.formatMessage(messages.verifyUser, { userName })
|
|
: intl.formatMessage(messages.unverifyUser, { userName });
|
|
|
|
if (window.confirm(promptMessage)) {
|
|
this.props.setVerifiedStatus!(userId, value);
|
|
}
|
|
};
|
|
|
|
onBlockUserClick = (e: Event) => {
|
|
const target = e.target as HTMLOptionElement;
|
|
// blur event will be triggered by the confirm pop-up which will start
|
|
// infinite loop of blur -> confirm -> blur -> ...
|
|
// so we trigger the blur event manually and have debounce mechanism to prevent it
|
|
if (e.type === 'change') {
|
|
target.blur();
|
|
}
|
|
// we have to debounce the blockUser function calls otherwise it will be
|
|
// called 2 times (by change event and by blur event)
|
|
this.blockUser(target.value as BlockTTL);
|
|
};
|
|
|
|
blockUser = debounce((ttl: BlockTTL): void => {
|
|
const { user } = this.props.data;
|
|
const blockingDurations = getBlockingDurations(this.props.intl);
|
|
const blockDuration = blockingDurations.find((el) => el.value === ttl);
|
|
// blocking duration may be undefined if user hasn't selected anything
|
|
// and ttl equals "Blocking period"
|
|
if (!blockDuration) return;
|
|
|
|
const duration = blockDuration.label;
|
|
const blockUser = this.props.intl.formatMessage(messages.blockUser, {
|
|
userName: user.name,
|
|
duration: duration.toLowerCase(),
|
|
});
|
|
if (window.confirm(blockUser)) {
|
|
this.props.blockUser!(user.id, user.name, ttl);
|
|
}
|
|
}, 100);
|
|
|
|
onUnblockUserClick = () => {
|
|
const { user } = this.props.data;
|
|
const unblockUser = this.props.intl.formatMessage(messages.unblockUser);
|
|
|
|
if (window.confirm(unblockUser)) {
|
|
this.props.unblockUser!(user.id);
|
|
}
|
|
};
|
|
|
|
deleteComment = () => {
|
|
const deleteComment = this.props.intl.formatMessage(messages.deleteMessage);
|
|
|
|
if (window.confirm(deleteComment)) {
|
|
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
|
|
|
this.props.removeComment!(this.props.data.id);
|
|
}
|
|
};
|
|
|
|
hideUser = () => {
|
|
const hideUserComment = this.props.intl.formatMessage(messages.hideUserComments, {
|
|
userName: this.props.data.user.name,
|
|
});
|
|
if (!window.confirm(hideUserComment)) return;
|
|
this.props.hideUser!(this.props.data.user);
|
|
};
|
|
|
|
addComment = async (text: string, title: string, pid?: CommentType['id']) => {
|
|
await this.props.addComment!(text, title, pid);
|
|
|
|
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
|
};
|
|
|
|
updateComment = async (id: CommentType['id'], text: string) => {
|
|
await this.props.updateComment!(id, text);
|
|
|
|
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
|
};
|
|
|
|
scrollToParent = (evt: Event) => {
|
|
const { pid } = this.props.data;
|
|
const parentCommentNode = document.getElementById(`${COMMENT_NODE_CLASSNAME_PREFIX}${pid}`);
|
|
|
|
evt.preventDefault();
|
|
|
|
if (!parentCommentNode) {
|
|
return;
|
|
}
|
|
|
|
const top = parentCommentNode.getBoundingClientRect().top;
|
|
|
|
if (postMessageToParent({ scrollTo: top })) {
|
|
return;
|
|
}
|
|
|
|
parentCommentNode.scrollIntoView();
|
|
};
|
|
|
|
get isVotesDisabled(): boolean {
|
|
return (
|
|
this.props.view !== 'main' ||
|
|
this.props.post_info?.read_only ||
|
|
this.props.data.delete ||
|
|
this.isCurrentUser() ||
|
|
this.isGuest() ||
|
|
(!StaticStore.config.anon_vote && this.isAnonymous())
|
|
);
|
|
}
|
|
|
|
copyComment = async () => {
|
|
const { name } = this.props.data.user;
|
|
const time = getLocalDatetime(this.props.intl, new Date(this.props.data.time));
|
|
const text = this.textNode.current?.textContent ?? '';
|
|
|
|
try {
|
|
await copy(`${name} ${time}\n${text}`);
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
|
|
this.setState({ isCopied: true }, () => {
|
|
setTimeout(() => this.setState({ isCopied: false }), 3000);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Defines whether current client is admin
|
|
*/
|
|
isAdmin = (): boolean => {
|
|
return !!this.props.user && this.props.user.admin;
|
|
};
|
|
|
|
/**
|
|
* Defines whether current client is not logged in
|
|
*/
|
|
isGuest = (): boolean => {
|
|
return !this.props.user;
|
|
};
|
|
|
|
/**
|
|
* Defines whether current client is logged in via `Anonymous provider`
|
|
*/
|
|
isAnonymous = (): boolean => {
|
|
return isUserAnonymous(this.props.user);
|
|
};
|
|
|
|
/**
|
|
* Defines whether comment made by logged in user
|
|
*/
|
|
isCurrentUser = (): boolean => {
|
|
if (this.isGuest()) return false;
|
|
|
|
return this.props.data.user.id === this.props.user!.id;
|
|
};
|
|
|
|
getCommentControls = (): JSX.Element[] => {
|
|
const isAdmin = this.isAdmin();
|
|
const isCurrentUser = this.isCurrentUser();
|
|
const controls: JSX.Element[] = [];
|
|
|
|
if (this.props.data.delete) {
|
|
return controls;
|
|
}
|
|
|
|
if (!(this.props.view === 'main' || this.props.view === 'pinned')) {
|
|
return controls;
|
|
}
|
|
|
|
if (isAdmin) {
|
|
controls.push(
|
|
this.state.isCopied ? (
|
|
<span className="comment__control comment__control_view_inactive comment__action">
|
|
<FormattedMessage id="comment.copied" defaultMessage="Copied!" />
|
|
</span>
|
|
) : (
|
|
<Button kind="link" onClick={this.copyComment} mix={['comment__control', 'comment__action']}>
|
|
<FormattedMessage id="comment.copy" defaultMessage="Copy" />
|
|
</Button>
|
|
),
|
|
<Button kind="link" onClick={this.togglePin} mix={['comment__control', 'comment__action']}>
|
|
{this.props.data.pin ? (
|
|
<FormattedMessage id="comment.unpin" defaultMessage="Unpin" />
|
|
) : (
|
|
<FormattedMessage id="comment.pin" defaultMessage="Pin" />
|
|
)}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
if (!isCurrentUser) {
|
|
controls.push(
|
|
<Button kind="link" onClick={this.hideUser} mix={['comment__control', 'comment__action']}>
|
|
<FormattedMessage id="comment.hide" defaultMessage="Hide" />
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
if (isAdmin) {
|
|
if (this.props.isUserBanned) {
|
|
controls.push(
|
|
<Button kind="link" onClick={this.onUnblockUserClick} mix={['comment__control', 'comment__action']}>
|
|
<FormattedMessage id="comment.unblock" defaultMessage="Unblock" />
|
|
</Button>
|
|
);
|
|
}
|
|
const blockingDurations = getBlockingDurations(this.props.intl);
|
|
if (this.props.user!.id !== this.props.data.user.id && !this.props.isUserBanned) {
|
|
controls.push(
|
|
<span className="comment__control comment__control_select-label">
|
|
<FormattedMessage id="comment.block" defaultMessage="Block" />
|
|
<select className="comment__control_select" onBlur={this.onBlockUserClick} onChange={this.onBlockUserClick}>
|
|
<option disabled selected value={undefined}>
|
|
<FormattedMessage id="comment.blocking-period" defaultMessage="Blocking period" />
|
|
</option>
|
|
{blockingDurations.map((block) => (
|
|
<option key={block.value} value={block.value}>
|
|
{block.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (!this.props.data.delete) {
|
|
controls.push(
|
|
<Button kind="link" onClick={this.deleteComment} mix={['comment__control', 'comment__action']}>
|
|
<FormattedMessage id="comment.delete" defaultMessage="Delete" />
|
|
</Button>
|
|
);
|
|
}
|
|
}
|
|
return controls;
|
|
};
|
|
|
|
render(props: CommentProps, state: State): JSX.Element {
|
|
const isAdmin = this.isAdmin();
|
|
const isGuest = this.isGuest();
|
|
const isCurrentUser = this.isCurrentUser();
|
|
|
|
const isReplying = props.editMode === CommentMode.Reply;
|
|
const isEditing = props.editMode === CommentMode.Edit;
|
|
const editable = props.repliesCount === 0 && state.editDeadline;
|
|
const uploadImageHandler = this.isAnonymous() ? undefined : this.props.uploadImage;
|
|
const commentControls = this.getCommentControls();
|
|
const intl = props.intl;
|
|
const CommentForm = this.props.CommentForm || null;
|
|
|
|
/**
|
|
* CommentType adapted for rendering
|
|
*/
|
|
|
|
const o = {
|
|
...props.data,
|
|
text:
|
|
props.view === 'preview'
|
|
? getTextSnippet(props.data.text)
|
|
: props.data.delete
|
|
? intl.formatMessage(messages.deletedComment)
|
|
: props.data.text,
|
|
time: new Date(props.data.time),
|
|
orig: isEditing
|
|
? props.data.orig &&
|
|
props.data.orig.replace(/&[#A-Za-z0-9]+;/gi, (entity) => {
|
|
const span = document.createElement('span');
|
|
span.innerHTML = entity;
|
|
return span.innerText;
|
|
})
|
|
: props.data.orig,
|
|
user: props.data.user,
|
|
};
|
|
|
|
const defaultMods = {
|
|
disabled: props.disabled,
|
|
pinned: props.data.pin,
|
|
// TODO: add default view mod or don't?
|
|
guest: isGuest,
|
|
view: props.view === 'main' || props.view === 'pinned' ? props.data.user.admin && 'admin' : props.view,
|
|
replying: props.view === 'main' && isReplying,
|
|
editing: props.view === 'main' && isEditing,
|
|
theme: props.view === 'preview' ? undefined : props.theme,
|
|
level: props.level,
|
|
collapsed: props.collapsed,
|
|
};
|
|
|
|
if (props.view === 'preview') {
|
|
return (
|
|
<article className={b('comment', { mix: props.mix }, defaultMods)}>
|
|
<div className="comment__body">
|
|
{!!o.title && (
|
|
<div className="comment__title">
|
|
<a className="comment__title-link" href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}>
|
|
{o.title}
|
|
</a>
|
|
</div>
|
|
)}
|
|
<div className="comment__info">
|
|
{!!o.title && o.user.name}
|
|
|
|
{!o.title && (
|
|
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__username">
|
|
{o.user.name}
|
|
</a>
|
|
)}
|
|
</div>{' '}
|
|
<div
|
|
className={b('comment__text', { mix: b('raw-content', {}, { theme: props.theme }) })}
|
|
// eslint-disable-next-line react/no-danger
|
|
dangerouslySetInnerHTML={{ __html: o.text }}
|
|
/>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
if (this.state.renderDummy && !props.editMode) {
|
|
const [width, height] = this.base
|
|
? [(this.base as Element).scrollWidth, (this.base as Element).scrollHeight]
|
|
: [100, 100];
|
|
return (
|
|
<article
|
|
id={props.disabled ? undefined : `${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}
|
|
style={{
|
|
width: `${width}px`,
|
|
height: `${height}px`,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
const goToParentMessage = intl.formatMessage(messages.goToParent);
|
|
|
|
return (
|
|
<article
|
|
className={b('comment', { mix: this.props.mix }, defaultMods)}
|
|
id={props.disabled ? undefined : `${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}
|
|
>
|
|
{props.view === 'user' && o.title && (
|
|
<div className="comment__title">
|
|
<a className="comment__title-link" href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}>
|
|
{o.title}
|
|
</a>
|
|
</div>
|
|
)}
|
|
<div className="comment__info">
|
|
{props.view !== 'user' && !props.collapsed && (
|
|
<div className="comment__avatar">
|
|
<Avatar url={o.user.picture} />
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.user}>
|
|
{props.view !== 'user' && (
|
|
<button onClick={() => this.toggleUserInfoVisibility()} className="comment__username">
|
|
{o.user.name}
|
|
</button>
|
|
)}
|
|
{isAdmin && props.view !== 'user' && (
|
|
<button
|
|
className={styles.verificationButton}
|
|
onClick={this.toggleVerify}
|
|
title={intl.formatMessage(messages.toggleVerification)}
|
|
>
|
|
<VerificationIcon
|
|
title={intl.formatMessage(o.user.verified ? messages.verifiedUser : messages.unverifiedUser)}
|
|
className={clsx(styles.verificationIcon, !o.user.verified && styles.verificationIconInactive)}
|
|
/>
|
|
</button>
|
|
)}
|
|
{!isAdmin && !!o.user.verified && props.view !== 'user' && (
|
|
<VerificationIcon className={styles.verificationIcon} title={intl.formatMessage(messages.verifiedUser)} />
|
|
)}
|
|
{o.user.paid_sub && (
|
|
<img
|
|
width={12}
|
|
height={12}
|
|
src={require('assets/social/patreon.svg').default}
|
|
alt={intl.formatMessage(messages.paidPatreon)}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__time">
|
|
{getLocalDatetime(this.props.intl, o.time)}
|
|
</a>
|
|
|
|
{!!props.level && props.level > 0 && props.view === 'main' && (
|
|
<a
|
|
className="comment__link-to-parent"
|
|
href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.pid}`}
|
|
aria-label={goToParentMessage}
|
|
title={goToParentMessage}
|
|
onClick={(e) => this.scrollToParent(e)}
|
|
>
|
|
{' '}
|
|
</a>
|
|
)}
|
|
|
|
{props.isUserBanned && props.view !== 'user' && (
|
|
<span className="comment__status">
|
|
<FormattedMessage id="comment.blocked-user" defaultMessage="Blocked" />
|
|
</span>
|
|
)}
|
|
|
|
{isAdmin && !props.isUserBanned && props.data.delete && (
|
|
<span className="comment__status">
|
|
<FormattedMessage id="comment.deleted-user" defaultMessage="Deleted" />
|
|
</span>
|
|
)}
|
|
{this.props.view !== 'pinned' && (
|
|
<CommentVotes
|
|
id={this.props.data.id}
|
|
vote={props.data.vote}
|
|
votes={props.data.score}
|
|
controversy={props.data.controversy}
|
|
disabled={this.isVotesDisabled}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="comment__body">
|
|
{(!props.collapsed || props.view === 'pinned') && (
|
|
<div
|
|
className={b('comment__text', { mix: b('raw-content', {}, { theme: props.theme }) })}
|
|
ref={this.textNode}
|
|
// eslint-disable-next-line react/no-danger
|
|
dangerouslySetInnerHTML={{ __html: o.text }}
|
|
/>
|
|
)}
|
|
|
|
{(!props.collapsed || props.view === 'pinned') && (
|
|
<div className="comment__actions">
|
|
{!props.data.delete && !props.isCommentsDisabled && !props.disabled && props.view === 'main' && (
|
|
<Button kind="link" onClick={this.toggleReplying} mix="comment__action">
|
|
{isReplying ? (
|
|
<FormattedMessage id="comment.cancel" defaultMessage="Cancel" />
|
|
) : (
|
|
<FormattedMessage id="comment.reply" defaultMessage="Reply" />
|
|
)}
|
|
</Button>
|
|
)}
|
|
{!props.data.delete &&
|
|
!props.disabled &&
|
|
!!o.orig &&
|
|
isCurrentUser &&
|
|
(editable || isEditing) &&
|
|
props.view === 'main' && [
|
|
<Button
|
|
key="edit-button"
|
|
kind="link"
|
|
{...getHandleClickProps(this.toggleEditing)}
|
|
mix={['comment__action', 'comment__action_type_edit']}
|
|
>
|
|
{isEditing ? (
|
|
<FormattedMessage id="comment.cancel" defaultMessage="Cancel" />
|
|
) : (
|
|
<FormattedMessage id="comment.edit" defaultMessage="Edit" />
|
|
)}
|
|
</Button>,
|
|
!isAdmin && (
|
|
<Button
|
|
key="delete-button"
|
|
kind="link"
|
|
{...getHandleClickProps(this.deleteComment)}
|
|
mix={['comment__action', 'comment__action_type_delete']}
|
|
>
|
|
<FormattedMessage id="comment.delete" defaultMessage="Delete" />
|
|
</Button>
|
|
),
|
|
state.editDeadline && (
|
|
<Countdown
|
|
key="countdown"
|
|
className="comment__edit-timer"
|
|
time={state.editDeadline}
|
|
onTimePassed={() =>
|
|
this.setState({
|
|
editDeadline: null,
|
|
})
|
|
}
|
|
/>
|
|
),
|
|
]}
|
|
|
|
{commentControls.length > 0 && <span className="comment__controls">{commentControls}</span>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{CommentForm && isReplying && props.view === 'main' && (
|
|
<CommentForm
|
|
id={o.id}
|
|
intl={this.props.intl}
|
|
user={props.user}
|
|
theme={props.theme}
|
|
mode="reply"
|
|
mix="comment__input"
|
|
onSubmit={(text: string, title: string) => this.addComment(text, title, o.id)}
|
|
onCancel={this.toggleReplying}
|
|
getPreview={this.props.getPreview!}
|
|
autofocus={true}
|
|
uploadImage={uploadImageHandler}
|
|
simpleView={StaticStore.config.simple_view}
|
|
/>
|
|
)}
|
|
|
|
{CommentForm && isEditing && props.view === 'main' && (
|
|
<CommentForm
|
|
id={o.id}
|
|
intl={this.props.intl}
|
|
user={props.user}
|
|
theme={props.theme}
|
|
value={o.orig}
|
|
mode="edit"
|
|
mix="comment__input"
|
|
onSubmit={(text: string) => this.updateComment(props.data.id, text)}
|
|
onCancel={this.toggleEditing}
|
|
getPreview={this.props.getPreview!}
|
|
errorMessage={state.editDeadline === null ? intl.formatMessage(messages.expiredTime) : undefined}
|
|
autofocus={true}
|
|
uploadImage={uploadImageHandler}
|
|
simpleView={StaticStore.config.simple_view}
|
|
/>
|
|
)}
|
|
</article>
|
|
);
|
|
}
|
|
}
|
|
|
|
function getTextSnippet(html: string) {
|
|
const LENGTH = 100;
|
|
const tmp = document.createElement('div');
|
|
tmp.innerHTML = html.replace('</p><p>', ' ');
|
|
|
|
const result = tmp.innerText || '';
|
|
const snippet = result.substr(0, LENGTH);
|
|
|
|
return snippet.length === LENGTH && result.length !== LENGTH ? `${snippet}...` : snippet;
|
|
}
|
|
|
|
function getLocalDatetime(intl: IntlShape, date: Date) {
|
|
return intl.formatMessage(messages.commentTime, {
|
|
day: intl.formatDate(date),
|
|
time: intl.formatTime(date),
|
|
});
|
|
}
|
|
|
|
const messages = defineMessages({
|
|
deleteMessage: {
|
|
id: 'comment.delete-message',
|
|
defaultMessage: 'Do you want to delete this comment?',
|
|
},
|
|
hideUserComments: {
|
|
id: 'comment.hide-user-comment',
|
|
defaultMessage: 'Do you want to hide comments of {userName}?',
|
|
},
|
|
pinComment: {
|
|
id: 'comment.pin-comment',
|
|
defaultMessage: 'Do you want to pin this comment?',
|
|
},
|
|
unpinComment: {
|
|
id: 'comment.unpin-comment',
|
|
defaultMessage: 'Do you want to unpin this comment?',
|
|
},
|
|
verifyUser: {
|
|
id: 'comment.verify-user',
|
|
defaultMessage: 'Do you want to verify {userName}?',
|
|
},
|
|
unverifyUser: {
|
|
id: 'comment.unverify-user',
|
|
defaultMessage: 'Do you want to unverify {userName}?',
|
|
},
|
|
blockUser: {
|
|
id: 'comment.block-user',
|
|
defaultMessage: 'Do you want to block {userName} {duration}?',
|
|
},
|
|
unblockUser: {
|
|
id: 'comment.unblock-user',
|
|
defaultMessage: 'Do you want to unblock this user?',
|
|
},
|
|
deletedComment: {
|
|
id: 'comment.deleted-comment',
|
|
defaultMessage: 'This comment was deleted',
|
|
},
|
|
|
|
toggleVerification: {
|
|
id: 'comment.toggle-verification',
|
|
defaultMessage: 'Toggle verification',
|
|
},
|
|
verifiedUser: {
|
|
id: 'comment.verified-user',
|
|
defaultMessage: 'Verified user',
|
|
},
|
|
unverifiedUser: {
|
|
id: 'comment.unverified-user',
|
|
defaultMessage: 'Unverified user',
|
|
},
|
|
goToParent: {
|
|
id: 'comment.go-to-parent',
|
|
defaultMessage: 'Go to parent comment',
|
|
},
|
|
expiredTime: {
|
|
id: 'comment.expired-time',
|
|
defaultMessage: 'Editing time has expired.',
|
|
},
|
|
commentTime: {
|
|
id: 'comment.time',
|
|
defaultMessage: '{day} at {time}',
|
|
},
|
|
paidPatreon: {
|
|
id: 'comment.paid-patreon',
|
|
defaultMessage: 'Patreon Paid Subscriber',
|
|
},
|
|
});
|