import { h, Component } from 'preact';
import api from 'common/api';
import { API_BASE, BASE_URL, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants';
import { url } from 'common/settings';
import store from 'common/store';
import Input from 'components/input';
import UserInfo from 'components/user-info';
export default class Comment extends Component {
constructor(props) {
super(props);
this.state = {
isReplying: false,
isEditing: false,
isUserVerified: false,
isUserInfoShown: false,
editTimeLeft: null,
};
this.votingPromise = Promise.resolve();
this.updateState(props);
this.decreaseScore = this.decreaseScore.bind(this);
this.increaseScore = this.increaseScore.bind(this);
this.toggleEditing = this.toggleEditing.bind(this);
this.toggleReplying = this.toggleReplying.bind(this);
this.toggleCollapse = this.toggleCollapse.bind(this);
this.toggleUserInfoVisibility = this.toggleUserInfoVisibility.bind(this);
this.scrollToParent = this.scrollToParent.bind(this);
this.onEdit = this.onEdit.bind(this);
this.onReply = this.onReply.bind(this);
this.onPinClick = this.onPinClick.bind(this);
this.onUnpinClick = this.onUnpinClick.bind(this);
this.onVerifyClick = this.onVerifyClick.bind(this);
this.onUnverifyClick = this.onUnverifyClick.bind(this);
this.onBlockClick = this.onBlockClick.bind(this);
this.onUnblockClick = this.onUnblockClick.bind(this);
this.onDeleteClick = this.onDeleteClick.bind(this);
}
componentWillReceiveProps(nextProps) {
this.updateState(nextProps);
}
updateState(props) {
const { data, data: { user: { block, id: commentUserId }, pin }, mods: { guest } = {} } = props;
const votes = data && data.votes || [];
const score = data && data.score || 0;
if (this.editTimerInterval) {
clearInterval(this.editTimerInterval);
this.editTimerInterval = null;
}
if (guest) {
this.setState({
guest,
score,
deleted: data ? data.delete : false,
});
} else {
const userId = store.get('user').id;
this.setState({
guest,
score,
pinned: !!pin,
deleted: data ? data.delete : false,
userBlocked: !!block,
scoreIncreased: userId in votes && votes[userId],
scoreDecreased: userId in votes && !votes[userId],
});
if (userId === commentUserId) {
const editDuration = store.get('config') && store.get('config').edit_duration;
const getEditTimeLeft = () => Math.floor(editDuration - (new Date() - new Date(data.time)) / 1000);
if (getEditTimeLeft() > 0) {
this.editTimerInterval = setInterval(() => {
const editTimeLeft = getEditTimeLeft();
if (editTimeLeft < 0) {
clearInterval(this.editTimerInterval);
this.editTimerInterval = null;
this.setState({ editTimeLeft: null });
} else {
this.setState({ editTimeLeft });
}
}, 1000);
}
}
}
}
toggleReplying() {
const { isReplying } = this.state;
const onPrevInputToggleCb = store.get('onPrevInputToggleCb');
this.setState({ isEditing: false }, () => this.setState({ isReplying: !isReplying }));
if (onPrevInputToggleCb) onPrevInputToggleCb();
if (!isReplying) {
store.set('onPrevInputToggleCb', () => this.setState({ isReplying: false }));
} else {
store.set('onPrevInputToggleCb', null);
}
}
toggleEditing() {
const { isEditing } = this.state;
const onPrevInputToggleCb = store.get('onPrevInputToggleCb');
this.setState({ isReplying: false }, () => this.setState({ isEditing: !isEditing }));
if (onPrevInputToggleCb) onPrevInputToggleCb();
if (!isEditing) {
store.set('onPrevInputToggleCb', () => this.setState({ isEditing: false }));
} else {
store.set('onPrevInputToggleCb', null);
}
}
toggleUserInfoVisibility() {
this.setState({ isUserInfoShown: !this.state.isUserInfoShown });
}
onPinClick() {
const { id } = this.props.data;
if (confirm('Do you want to pin this comment?')) {
this.setState({ pinned: true });
api.pin({ id, url }).then(() => {
api.getComment({ id }).then(comment => store.replaceComment(comment));
});
}
}
onUnpinClick() {
const { id } = this.props.data;
if (confirm('Do you want to unpin this comment?')) {
this.setState({ pinned: false });
api.unpin({ id, url }).then(() => {
api.getComment({ id }).then(comment => store.replaceComment(comment));
});
}
}
onVerifyClick() {
const { id, user: { id: userId } } = this.props.data;
if (confirm('Do you want to verify this user?')) {
this.setState({ isUserVerified: true });
api.verify({ id: userId }).then(() => {
api.getComment({ id }).then(comment => store.replaceComment(comment));
});
}
}
onUnverifyClick() {
const { id, user: { id: userId } } = this.props.data;
if (confirm('Do you want to unverify this user?')) {
this.setState({ isUserVerified: false });
api.unverify({ id: userId }).then(() => {
api.getComment({ id }).then(comment => store.replaceComment(comment));
});
}
}
onBlockClick() {
const { id, user: { id: userId } } = this.props.data;
if (confirm('Do you want to block this user?')) {
this.setState({ userBlocked: true });
api.blockUser({ id: userId }).then(() => {
api.getComment({ id }).then(comment => store.replaceComment(comment));
});
}
}
onUnblockClick() {
const { id, user: { id: userId } } = this.props.data;
if (confirm('Do you want to unblock this user?')) {
this.setState({ userBlocked: false });
api.unblockUser({ id: userId }).then(() => {
api.getComment({ id }).then(comment => store.replaceComment(comment));
});
}
}
onDeleteClick() {
const { id } = this.props.data;
if (confirm('Do you want to delete this comment?')) {
this.setState({
deleted: true,
isEditing: false,
isReplying: false,
});
api.remove({ id }).then(() => {
api.getComment({ id }).then(comment => store.replaceComment(comment));
});
}
}
increaseScore() {
const { score, scoreIncreased, scoreDecreased } = this.state;
const { id } = this.props.data;
if (scoreIncreased) return;
this.setState({
scoreIncreased: !scoreDecreased,
scoreDecreased: false,
score: score + 1,
});
this.votingPromise = this.votingPromise
.then(() => {
return api.vote({ id, url, value: 1 })
.then(() => {
api.getComment({ id }).then(comment => store.replaceComment(comment));
});
});
}
decreaseScore() {
const { score, scoreIncreased, scoreDecreased } = this.state;
const { id } = this.props.data;
if (scoreDecreased) return;
this.setState({
scoreDecreased: !scoreIncreased,
scoreIncreased: false,
score: score - 1,
});
this.votingPromise = this.votingPromise
.then(() => {
return api.vote({ id, url, value: -1 })
.then(() => {
api.getComment({ id }).then(comment => store.replaceComment(comment));
});
});
}
onReply(...rest) {
this.props.onReply(...rest);
this.setState({
isReplying: false,
});
}
onEdit(...rest) {
this.props.onEdit(...rest);
this.setState({
isEditing: false,
});
}
scrollToParent(e) {
const { data: { pid } } = this.props;
e.preventDefault();
const parentCommentNode = document.getElementById(`${COMMENT_NODE_CLASSNAME_PREFIX}${pid}`);
if (parentCommentNode) {
parentCommentNode.scrollIntoView();
}
}
toggleCollapse() {
this.setState({
isEditing: false,
isReplying: false,
});
if (this.props.onCollapseToggle) {
this.props.onCollapseToggle();
}
}
render(props, {
guest,
userBlocked,
pinned,
score,
scoreIncreased,
scoreDecreased,
deleted,
isReplying,
isEditing,
isUserVerified,
editTimeLeft,
isUserInfoShown,
}) {
const { data, mods = {} } = props;
const isAdmin = !guest && store.get('user').admin;
const isGuest = guest || !Object.keys(store.get('user')).length;
const isCurrentUser = (data.user && data.user.id) === (store.get('user') && store.get('user').id);
const config = store.get('config') || {};
const lowCommentScore = config.low_score;
const o = {
...data,
text:
data.text.length
? (mods.view === 'preview' ? getTextSnippet(data.text) : data.text)
: (
userBlocked
? 'This user was blocked'
: (
deleted
? 'This comment was deleted'
: data.text
)
),
time: formatTime(new Date(data.time)),
orig: isEditing
? (
data.orig && data.orig
.replace(/&[#A-Za-z0-9]+;/gi, entity => {
const span = document.createElement('span');
span.innerHTML = entity;
return span.innerText;
})
)
: data.orig,
score: {
value: Math.abs(score),
sign: score > 0 ? '+' : (score < 0 ? '−' : null),
view: score > 0 ? 'positive' : (score < 0 ? 'negative' : null),
},
user: {
...data.user,
picture: data.user.picture.indexOf(API_BASE) === 0 ? `${BASE_URL}${data.user.picture}` : data.user.picture,
isDefaultPicture: !data.user.picture.length,
verified: data.user.verified || isUserVerified,
},
};
const defaultMods = {
pinned,
// TODO: we also have critical_score, so we need to collapse comments with it in future
useless: userBlocked || deleted || (score <= lowCommentScore && !mods.pinned && !mods.disabled),
// TODO: add default view mod or don't?
view: o.user.admin ? 'admin' : null,
replying: isReplying,
editing: isEditing,
};
if (mods.view === 'preview') {
return (
)
}
{
mods.view !== 'user' && (
{o.user.name}
)
}
{
isAdmin && mods.view !== 'user' && (
)
}
{
!isAdmin && !!o.user.verified && mods.view !== 'user' && (
)
}
{o.time}
{
mods.level > 0 && mods.view !== 'user' && (
)
}
{
isAdmin && userBlocked && mods.view !== 'user' && (
Blocked
)
}
{
isAdmin && !userBlocked && deleted && (
Deleted
)
}
{
!mods.disabled && mods.view !== 'user' && (
{mods.collapsed ? '+' : '−'}
)
}
Vote up
{o.score.sign}{o.score.value}
Vote down
', ' '); const result = tmp.innerText || ''; const snippet = result.substr(0, LENGTH); return (snippet.length === LENGTH && result.length !== LENGTH) ? `${snippet}...` : snippet; } function formatTime(time) { // 'ru-RU' adds a dot as a separator const date = time.toLocaleDateString(['ru-RU'], { day: '2-digit', month: '2-digit', year: '2-digit' }); // do it manually because Intl API doesn't add leading zeros to hours; idk why const hours = `0${time.getHours()}`.slice(-2); const mins = `0${time.getMinutes()}`.slice(-2); return `${date} at ${hours}:${mins}`; }