rewrite vote component

This commit is contained in:
Paul Mineev
2022-04-13 12:51:38 -05:00
committed by Umputun
parent a290d906ee
commit 0e4ae6e050
53 changed files with 885 additions and 882 deletions
+7 -2
View File
@@ -16,8 +16,13 @@ export const getUserComments = (
config: { limit: number; skip?: number } = { limit: 10, skip: 0 }
): Promise<{ comments: Comment[]; count: number }> => apiFetcher.get('/comments', { user: userId, ...config });
export const putCommentVote = ({ id, value }: { id: Comment['id']; value: number }): Promise<void> =>
apiFetcher.put(`/vote/${id}`, { url, vote: value });
export const putCommentVote = ({
id,
vote,
}: {
id: Comment['id'];
vote: number;
}): Promise<Pick<Comment, 'id' | 'score'>> => apiFetcher.put(`/vote/${id}`, { url, vote });
export const addComment = ({
title,
@@ -1,7 +0,0 @@
.comment__score-value {
display: inline-block;
min-width: 24px;
vertical-align: middle;
text-align: center;
font-size: 14px;
}
@@ -1,12 +0,0 @@
.comment__score {
position: absolute;
top: 5px;
right: 0;
min-width: 60px;
font-size: 0;
font-weight: 700;
text-align: right;
line-height: 16px;
user-select: none;
cursor: default;
}
@@ -1,10 +0,0 @@
.comment__vote_disabled {
&.comment__vote_type_up,
&.comment__vote_type_down {
&,
&:hover {
background-image: url('./comment__vote_disabled.svg');
cursor: not-allowed;
}
}
}
@@ -1,3 +0,0 @@
<svg width="14" height="18" xmlns="http://www.w3.org/2000/svg">
<path fill="#ededed" d="M4.426 17.116V7.72H.394L7 .988l6.606 6.732H9.574v9.396z" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 176 B

@@ -1,3 +0,0 @@
.comment__vote_selected {
cursor: default;
}
@@ -1,9 +0,0 @@
.comment__vote_type_down {
transform: scale(1, -1);
margin-left: 4px;
&.comment__vote_selected,
&:hover {
background-image: url('./comment__vote_type_down.svg');
}
}
@@ -1,3 +0,0 @@
<svg width="14" height="18" xmlns="http://www.w3.org/2000/svg">
<path fill="#cc0606" d="M4.426 17.116V7.72H.394L7 .988l6.606 6.732H9.574v9.396z" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 176 B

@@ -1,8 +0,0 @@
.comment__vote_type_up {
margin-right: 4px;
&.comment__vote_selected,
&:hover {
background-image: url('./comment__vote_type_up.svg');
}
}
@@ -1,3 +0,0 @@
<svg width="14" height="18" xmlns="http://www.w3.org/2000/svg">
<path fill="#259e06" d="M4.426 17.116V7.72H.394L7 .988l6.606 6.732H9.574v9.396z" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 176 B

@@ -1,19 +0,0 @@
.comment__vote {
display: inline-block;
width: 16px;
height: 14px;
vertical-align: top;
font-size: 0;
line-height: 0;
background: url('./comment__vote.svg') center no-repeat;
background-size: contain;
cursor: pointer;
}
.voting__error {
color: var(--color25);
text-align: right;
font-size: 14px;
line-height: 1;
margin-top: -10px;
}
@@ -1,3 +0,0 @@
<svg width="14" height="18" xmlns="http://www.w3.org/2000/svg">
<path fill="#dedede" d="M4.426 17.116V7.72H.394L7 .988l6.606 6.732H9.574v9.396z" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 176 B

@@ -24,8 +24,4 @@
& .comment__time {
font-style: italic;
}
& .comment__score {
top: 3px;
}
}
@@ -3,11 +3,6 @@
display: none;
}
& .comment__score {
top: 4px;
right: 0;
}
/* it isn't mobile first, but it's fine here */
@media (-moz-touch-enabled: 1) and (max-width: 768px), (pointer: coarse) and (max-width: 768px) {
border: 8px solid;
@@ -23,10 +18,5 @@
border-bottom-width: 0;
border-top-width: 8px;
}
& .comment__score {
top: 12px;
right: 8px;
}
}
}
@@ -3,11 +3,6 @@
position: relative;
z-index: 1;
& .comment__score {
top: 4px;
right: 0;
}
/* it isn't mobile first, but it's fine here */
@media (-moz-touch-enabled: 1) and (max-width: 768px), (pointer: coarse) and (max-width: 768px) {
border: 8px solid;
@@ -26,10 +21,5 @@
border-bottom-width: 0;
border-top-width: 8px;
}
& .comment__score {
top: 5px;
right: 8px;
}
}
}
@@ -40,18 +40,6 @@
}
}
& .comment__score {
color: var(--color31);
}
& .comment__score_view_negative {
color: var(--color30);
}
& .comment__score_view_positive {
color: var(--color12);
}
& .comment__status {
color: var(--color11);
}
@@ -40,18 +40,6 @@
}
}
& .comment__score {
color: var(--color31);
}
& .comment__score_view_negative {
color: var(--color30);
}
& .comment__score_view_positive {
color: var(--color12);
}
& .comment__status {
color: var(--color11);
}
@@ -0,0 +1,70 @@
.root {
display: flex;
align-items: center;
position: absolute;
top: 5px;
right: 0;
min-width: 72px;
font-weight: 700;
text-align: right;
line-height: 16px;
user-select: none;
}
.rootDisabled {
justify-content: center;
}
.voteButton {
display: flex;
align-items: center;
padding: 2px;
opacity: 0.4;
color: var(--color13);
transition: opacity 0.15s color 0.15s;
}
.root:hover .voteButton {
opacity: 1;
}
.upVoteButton:hover,
.upVoteButtonActive {
color: rgb(var(--color12));
opacity: 1;
}
.downVoteButton:hover,
.downVoteButtonActive {
color: rgb(var(--color30));
opacity: 1;
}
.votes {
margin: 0 4px;
padding: 2px 6px;
font-weight: 700;
border-radius: 3px;
min-width: 16px;
text-align: center;
color: rgb(var(--secondary-darker-text-color));
}
.votesNegative {
color: rgb(var(--color30));
background-color: rgba(var(--color30), 0.1);
}
.votesPositive {
color: rgb(var(--color12));
background-color: rgba(var(--color12), 0.1);
}
.upVoteIcon {
transform: rotate(180deg);
}
.errorMessage {
white-space: nowrap;
font-weight: 500;
}
@@ -0,0 +1,116 @@
import { h } from 'preact';
import '@testing-library/jest-dom';
import { fireEvent, screen, waitFor } from '@testing-library/preact';
import { render } from 'tests/utils';
import * as api from 'common/api';
import { CommentVotes } from './comment-votes';
import { StaticStore } from 'common/static-store';
describe('<CommentVote />', () => {
it('should render vote component', () => {
render(<CommentVotes id="1" vote={0} votes={0} controversy={0} />);
expect(screen.getByTitle('Vote up')).toBeVisible();
expect(screen.getByTitle('Vote down')).toBeVisible();
expect(screen.getByTitle('Votes score')).toBeVisible();
// expect(screen.getByTitle('Votes score')).toHaveAttribute('title', '0.00');
});
it('should render vote component with positive score', () => {
render(<CommentVotes id="1" vote={0} votes={1} controversy={0} />);
expect(screen.getByTitle('Votes score')).toBeVisible();
});
it('should render vote component with negative score', () => {
render(<CommentVotes id="1" vote={0} votes={-1} controversy={0} />);
expect(screen.getByTitle('Votes score')).toBeVisible();
});
it('should disable buttons after upvote when request is in progress', () => {
jest.spyOn(api, 'putCommentVote').mockImplementationOnce(jest.fn(() => new Promise(() => {})));
render(<CommentVotes id="1" vote={0} votes={10} controversy={0} />);
fireEvent(screen.getByTitle('Vote up'), new Event('click'));
expect(screen.getByTitle('Vote down')).toBeDisabled();
expect(screen.getByTitle('Vote up')).toBeDisabled();
});
it('should disable upvote button when upvoted', () => {
render(<CommentVotes id="1" vote={1} votes={10} controversy={0} />);
expect(screen.getByTitle('Vote up')).toBeDisabled();
});
it('should disable downvote button when downvoted', () => {
render(<CommentVotes id="1" vote={-1} votes={10} controversy={0} />);
expect(screen.getByTitle('Vote down')).toBeDisabled();
});
it('should disable buttons after downvote when request is in progress', async () => {
jest.spyOn(api, 'putCommentVote').mockImplementationOnce(jest.fn(() => new Promise(() => {})));
render(<CommentVotes id="1" vote={0} votes={10} controversy={0} />);
fireEvent(screen.getByTitle('Vote down'), new Event('click'));
expect(screen.getByTitle('Vote down')).toBeDisabled();
expect(screen.getByTitle('Vote up')).toBeDisabled();
});
it.each([
['upvote', 1, 'Vote up', 'Vote down', 'upVoteButtonActive'],
['downvote', -1, 'Vote down', 'Vote up', 'downVoteButtonActive'],
])(
'should go throught voting process and communicate with store when %s button is clicked',
async (_, increment, activeButtonText, secondButtonText, activeButtonClass) => {
const putCommentVoteSpy = jest
.spyOn(api, 'putCommentVote')
.mockImplementationOnce(({ vote }) => Promise.resolve({ id: '1', score: 10 + vote }));
render(<CommentVotes id="1" vote={0} votes={10} controversy={0} />);
fireEvent(screen.getByTitle(activeButtonText), new Event('click'));
expect(screen.getByTitle('Votes score')).toHaveTextContent(`${10 + increment}`);
expect(screen.getByTitle(activeButtonText)).toBeDisabled();
expect(screen.getByTitle(secondButtonText)).toBeDisabled();
await waitFor(() => expect(putCommentVoteSpy).toHaveBeenCalledWith({ id: '1', vote: increment }));
}
);
it('should render tooltip with error when request failed', async () => {
let reject = (_: { code: number }) => {};
const putCommnetVoteSpy = jest.spyOn(api, 'putCommentVote').mockImplementationOnce(
jest.fn(
() =>
new Promise((_, r) => {
reject = r;
}) as Promise<{ id: string; score: number }>
)
);
render(<CommentVotes id="1" vote={0} votes={10} controversy={0} />);
expect(screen.getByTitle('Votes score')).toHaveTextContent('10');
fireEvent(screen.getByTitle('Vote down'), new Event('click'));
await waitFor(() => expect(screen.getByTitle('Votes score')).toHaveTextContent('9'));
reject({ code: 0 });
await waitFor(() => {
expect(screen.getByTitle('Votes score')).toHaveTextContent('10');
expect(screen.getByText('Something went wrong. Please try again a bit later.')).toBeVisible();
});
putCommnetVoteSpy.mockClear();
});
it('should render without voting buttons', () => {
render(<CommentVotes id="1" vote={0} votes={10} controversy={0} disabled={true} />);
expect(screen.queryByTitle('Vote down')).not.toBeInTheDocument();
expect(screen.queryByTitle('Vote up')).not.toBeInTheDocument();
});
it('should allow only upvote ability', () => {
StaticStore.config.positive_score = true;
render(<CommentVotes id="1" vote={0} votes={10} controversy={0} />);
expect(screen.queryByTitle('Vote down')).not.toBeInTheDocument();
expect(screen.getByTitle('Vote up')).toBeVisible();
});
it('should disable downvote ability when `low_score` is reached', async () => {
StaticStore.config.low_score = -4;
jest.spyOn(api, 'putCommentVote').mockImplementation(jest.fn(async () => ({ id: '1', score: -4 })));
render(<CommentVotes id="1" vote={0} votes={-3} controversy={0} />);
expect(screen.getByTitle('Vote down')).not.toBeDisabled();
fireEvent(screen.getByTitle('Vote down'), new Event('click'));
await waitFor(() => {
expect(screen.getByTitle('Vote down')).toBeDisabled();
});
});
});
@@ -0,0 +1,118 @@
import clsx from 'clsx';
import { h } from 'preact';
import { useState } from 'preact/hooks';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { patchComment } from 'store/comments/actions';
import { putCommentVote } from 'common/api';
import { StaticStore } from 'common/static-store';
import { ArrowIcon } from 'components/icons/arrow';
import styles from './comment-votes.module.css';
import { Tooltip } from 'components/tooltip';
import { extractErrorMessageFromResponse } from 'utils/errorUtils';
type Props = {
id: string;
vote: 0 | -1 | 1;
votes: number;
controversy: number | undefined;
disabled?: boolean;
};
export function CommentVotes({ id, votes, vote, disabled, controversy = 0 }: Props) {
const intl = useIntl();
const dispatch = useDispatch();
const [loadingState, setLoadingState] = useState<{ vote: number; votes: number } | null>(null);
const [errorMessage, setErrorMessage] = useState<string | void>(undefined);
async function handleClick(evt: preact.JSX.TargetedEvent<HTMLButtonElement>) {
const { value } = evt.currentTarget.dataset;
const increment = Number(value) as -1 | 1;
setLoadingState({ vote: vote + increment, votes: votes + increment });
try {
const p = await putCommentVote({ id, vote: increment });
dispatch(patchComment({ ...p, vote: (vote + increment) as -1 | 0 | 1 }));
setErrorMessage(undefined);
setTimeout(() => setLoadingState(null), 200);
} catch (err) {
// @ts-ignore
setErrorMessage(extractErrorMessageFromResponse(err, intl));
setLoadingState(null);
}
}
const lowScore = StaticStore.config.low_score === votes;
const positiveScore = StaticStore.config.positive_score;
const isUpvoted = vote === 1;
const isDownvoted = vote === -1;
return (
<span className={clsx(styles.root, disabled && styles.rootDisabled)}>
{Boolean(!disabled && !positiveScore) && (
<button
className={clsx(styles.voteButton, styles.downVoteButton, isDownvoted && styles.downVoteButtonActive)}
onClick={handleClick}
data-value={-1}
title={intl.formatMessage(messages.downvote)}
disabled={lowScore || loadingState !== null || isDownvoted}
>
<ArrowIcon className={styles.downVoteIcon} />
</button>
)}
<Tooltip
content={errorMessage ? <div class={styles.errorMessage}>{errorMessage}</div> : undefined}
position="top-left"
hideBehavior="mouseleave"
hideTimeout={10000}
permanent
onHide={() => {
setErrorMessage(undefined);
}}
>
<div
title={intl.formatMessage(messages.score)}
// title={intl.formatMessage(messages.controversy, { value: controversy })}
className={clsx(styles.votes, {
[styles.votesNegative]: votes < 0,
[styles.votesPositive]: votes > 0,
})}
>
{loadingState?.votes ?? votes}
</div>
</Tooltip>
{!disabled && (
<button
className={clsx(styles.voteButton, styles.upVoteButton, isUpvoted && styles.upVoteButtonActive)}
onClick={handleClick}
data-value={1}
title={intl.formatMessage(messages.upvote)}
disabled={loadingState !== null || isUpvoted}
>
<ArrowIcon className={styles.upVoteIcon} />
</button>
)}
</span>
);
}
export const messages = defineMessages({
score: {
id: 'vote.score',
defaultMessage: 'Votes score',
},
upvote: {
id: 'vote.upvote',
defaultMessage: 'Vote up',
},
downvote: {
id: 'vote.downvote',
defaultMessage: 'Vote down',
},
controversy: {
id: 'vote.controversy',
defaultMessage: 'Controversy: {value}',
},
});
+115 -177
View File
@@ -4,14 +4,14 @@ import '@testing-library/jest-dom';
import { screen } from '@testing-library/preact';
import { render } from 'tests/utils';
import { useIntl, IntlProvider } from 'react-intl';
import { useIntl, IntlProvider, IntlShape } from 'react-intl';
import { Provider } from 'react-redux';
import enMessages from 'locales/en.json';
import type { User, Comment as CommentType, PostInfo } from 'common/types';
import { StaticStore } from 'common/static-store';
import { sleep } from 'utils/sleep';
import { Comment, CommentProps } from './comment';
import { mockStore } from '__stubs__/store';
function CommentWithIntl(props: CommentProps) {
return <Comment {...props} intl={useIntl()} />;
@@ -21,43 +21,67 @@ function CommentWithIntl(props: CommentProps) {
function mountComment(props: CommentProps) {
return mount(
<IntlProvider locale="en" messages={enMessages}>
<CommentWithIntl {...props} />
<Provider store={mockStore({})}>
<CommentWithIntl {...props} />
</Provider>
</IntlProvider>
);
}
function getDefaultProps() {
function getProps(): CommentProps {
return {
isCommentsDisabled: false,
theme: 'light',
post_info: {
url: 'http://localhost/post/1',
count: 2,
read_only: false,
} as PostInfo,
},
view: 'main',
data: {
id: 'comment_id',
text: 'test comment',
vote: 0,
user: {
id: 'someone',
name: 'username',
picture: 'somepicture-url',
},
time: new Date().toString(),
pid: 'parent_id',
score: 0,
voted_ips: [],
locator: {
url: 'somelocatorurl',
site: 'remark',
},
} as CommentType,
user: {
id: 'someone',
picture: 'http://localhost/somepicture-url',
name: 'username',
ip: '',
admin: false,
block: false,
verified: false,
},
},
user: {
admin: false,
id: 'testuser',
picture: 'somepicture-url',
} as User,
} as CommentProps & { user: User };
picture: 'http://localhost/testuser-url',
name: 'test',
ip: '',
admin: false,
block: false,
verified: false,
},
intl: {} as IntlShape,
};
}
const DefaultProps = getDefaultProps();
describe('<Comment />', () => {
let props = getProps();
beforeEach(() => {
props = getProps();
});
it('should render patreon subscriber icon', async () => {
const props = getDefaultProps() as CommentProps;
const props = getProps();
props.data.user.paid_sub = true;
render(<CommentWithIntl {...props} />);
@@ -68,28 +92,25 @@ describe('<Comment />', () => {
describe('verification', () => {
it('should render active verification icon', () => {
const props = getDefaultProps();
props.data.user.verified = true;
render(<CommentWithIntl {...props} />);
expect(screen.getByTitle('Verified user')).toBeVisible();
});
it('should not render verification icon', () => {
const props = getDefaultProps();
const props = getProps();
render(<CommentWithIntl {...props} />);
expect(screen.queryByTitle('Verified user')).not.toBeInTheDocument();
});
it('should render verification button for admin', () => {
const props = getDefaultProps();
props.user.admin = true;
props.user!.admin = true;
render(<CommentWithIntl {...props} />);
expect(screen.getByTitle('Toggle verification')).toBeVisible();
});
it('should render active verification icon for admin', () => {
const props = getDefaultProps();
props.user.admin = true;
props.user!.admin = true;
props.data.user.verified = true;
render(<CommentWithIntl {...props} />);
expect(screen.queryByTitle('Verified user')).toBeVisible();
@@ -97,154 +118,77 @@ describe('<Comment />', () => {
});
describe('voting', () => {
it('should be disabled for an anonymous user', () => {
const wrapper = mountComment({ ...DefaultProps, user: { id: 'anonymous_1' } } as CommentProps);
const voteButtons = wrapper.find('.comment__vote');
let props = getProps();
expect(voteButtons.length).toEqual(2);
voteButtons.forEach((button) => {
expect(button.prop('aria-disabled')).toEqual('true');
expect(button.prop('title')).toEqual("Anonymous users can't vote");
});
beforeEach(() => {
props = getProps();
});
it('should be enabled for an anonymous user when it was allowed from server', () => {
StaticStore.config.anon_vote = true;
const wrapper = mountComment({ ...DefaultProps, user: { id: 'anonymous_1' } } as CommentProps);
const voteButtons = wrapper.find('.comment__vote');
expect(voteButtons.length).toEqual(2);
voteButtons.forEach((button) => {
expect(button.prop('aria-disabled')).toEqual('false');
});
it('should render vote component', () => {
render(<CommentWithIntl {...props} />);
expect(screen.getByTitle('Votes score')).toBeVisible();
});
it('disabled on user info widget', () => {
const element = mountComment({ ...DefaultProps, view: 'user' } as CommentProps);
const voteButtons = element.find('.comment__vote');
expect(voteButtons.length).toStrictEqual(2);
voteButtons.forEach((b) => {
expect(b.getDOMNode().getAttribute('aria-disabled')).toStrictEqual('true');
expect(b.getDOMNode().getAttribute('title')).toStrictEqual("Voting allowed only on post's page");
});
});
it('disabled on read only post', () => {
const element = mountComment({
...DefaultProps,
post_info: { ...DefaultProps.post_info, read_only: true },
} as CommentProps);
const voteButtons = element.find('.comment__vote');
expect(voteButtons.length).toStrictEqual(2);
voteButtons.forEach((b) => {
expect(b.getDOMNode().getAttribute('aria-disabled')).toStrictEqual('true');
expect(b.getDOMNode().getAttribute('title')).toStrictEqual("Can't vote on read-only topics");
});
});
it('disabled for deleted comment', () => {
const element = mountComment({ ...DefaultProps, data: { ...DefaultProps.data, delete: true } } as CommentProps);
const voteButtons = element.find('.comment__vote');
expect(voteButtons.length).toStrictEqual(2);
voteButtons.forEach((b) => {
expect(b.getDOMNode().getAttribute('aria-disabled')).toStrictEqual('true');
expect(b.getDOMNode().getAttribute('title')).toStrictEqual("Can't vote for deleted comment");
});
});
it('disabled for guest', () => {
const element = mountComment({
...DefaultProps,
user: {
id: 'someone',
picture: 'somepicture-url',
it.each([
[
'when the comment is pinned',
() => {
props.view = 'pinned';
},
} as CommentProps);
const voteButtons = element.find('.comment__vote');
expect(voteButtons.length).toStrictEqual(2);
voteButtons.forEach((b) => {
expect(b.getDOMNode().getAttribute('aria-disabled')).toStrictEqual('true');
expect(b.getDOMNode().getAttribute('title')).toStrictEqual("Can't vote for your own comment");
});
});
it('disabled for own comment', () => {
const element = mountComment({ ...DefaultProps, user: null } as CommentProps);
const voteButtons = element.find('.comment__vote');
expect(voteButtons.length).toStrictEqual(2);
voteButtons.forEach((b) => {
expect(b.getDOMNode().getAttribute('aria-disabled')).toStrictEqual('true');
expect(b.getDOMNode().getAttribute('title')).toStrictEqual('Sign in to vote');
});
});
it('disabled for already upvoted comment', async () => {
const voteSpy = jest.fn(async () => undefined);
const element = mountComment({
...DefaultProps,
data: { ...DefaultProps.data, vote: +1 } as CommentProps['data'],
putCommentVote: voteSpy,
} as CommentProps);
const voteButtons = element.find('.comment__vote');
expect(voteButtons.length).toStrictEqual(2);
expect(voteButtons.at(0).getDOMNode().getAttribute('aria-disabled')).toStrictEqual('true');
voteButtons.at(0).simulate('click');
await sleep(100);
expect(voteSpy).not.toBeCalled();
expect(voteButtons.at(1).getDOMNode().getAttribute('aria-disabled')).toStrictEqual('false');
voteButtons.at(1).simulate('click');
await sleep(100);
expect(voteSpy).toBeCalled();
}, 30000);
it('disabled for already downvoted comment', async () => {
const voteSpy = jest.fn(async () => undefined);
const element = mountComment({
...DefaultProps,
data: {
...DefaultProps.data,
vote: -1,
],
[
'when rendered in profile',
() => {
props.view = 'user';
},
putCommentVote: voteSpy,
} as CommentProps);
const voteButtons = element.find('.comment__vote');
expect(voteButtons.length).toStrictEqual(2);
expect(voteButtons.at(1).getDOMNode().getAttribute('aria-disabled')).toStrictEqual('true');
voteButtons.at(1).simulate('click');
await sleep(100);
expect(voteSpy).not.toBeCalled();
expect(voteButtons.at(0).getDOMNode().getAttribute('aria-disabled')).toStrictEqual('false');
voteButtons.at(0).simulate('click');
await sleep(100);
expect(voteSpy).toBeCalled();
}, 30000);
],
[
'when rendered in preview',
() => {
props.view = 'preview';
},
],
[
'when post is read only',
() => {
props.post_info!.read_only = true;
},
],
[
'when comment was deleted',
() => {
props.data.delete = true;
},
],
[
'on current user comments',
() => {
props.user!.id = 'testuser';
props.data.user.id = 'testuser';
},
],
[
'for guest users',
() => {
props.user = null;
},
],
[
'for anonymous users',
() => {
props.user!.id = 'anonymous_1';
},
],
])('should not render vote component %s', (_, action) => {
action();
render(<CommentWithIntl {...props} />);
expect(screen.queryByText('Votes score')).not.toBeInTheDocument();
});
});
describe('admin controls', () => {
it('for admin if shows admin controls', () => {
const element = mountComment({ ...DefaultProps, user: { ...DefaultProps.user, admin: true } } as CommentProps);
props.user!.admin = true;
const element = mountComment(props);
const controls = element.find('.comment__controls').children();
expect(controls.length).toBe(5);
expect(controls.at(0).text()).toEqual('Copy');
expect(controls.at(1).text()).toEqual('Pin');
@@ -254,7 +198,7 @@ describe('<Comment />', () => {
});
it('for regular user it shows only "hide"', () => {
const element = mountComment({ ...DefaultProps, user: { ...DefaultProps.user, admin: false } } as CommentProps);
const element = mountComment(props);
const controls = element.find('.comment__controls').children();
expect(controls.length).toBe(1);
@@ -264,7 +208,6 @@ describe('<Comment />', () => {
it('should be editable', async () => {
StaticStore.config.edit_duration = 300;
const props = getDefaultProps();
props.repliesCount = 0;
props.user!.id = '100';
props.data.user.id = '100';
@@ -283,20 +226,15 @@ describe('<Comment />', () => {
it('should not be editable', () => {
StaticStore.config.edit_duration = 300;
Object.assign(props.data, {
user: props.user,
id: '100',
vote: 1,
time: new Date(new Date().getDate() - 300).toString(),
orig: 'test',
});
const component = mountComment({
...DefaultProps,
user: DefaultProps.user as User,
data: {
...DefaultProps.data,
id: '100',
user: DefaultProps.user as User,
vote: 1,
time: new Date(new Date().getDate() - 300).toString(),
orig: 'test',
} as CommentType,
} as CommentProps);
const component = mountComment(props);
expect(component.find('Comment').state('editDeadline')).toBe(null);
});
});
+25 -163
View File
@@ -10,7 +10,6 @@ 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 { extractErrorMessageFromResponse, FetcherError } from 'utils/errorUtils';
import { isUserAnonymous } from 'utils/isUserAnonymous';
import { CommentFormProps } from 'components/comment-form';
@@ -20,9 +19,9 @@ import { Countdown } from 'components/countdown';
import { VerificationIcon } from 'components/icons/verification';
import { getPreview, uploadImage } from 'common/api';
import { postMessageToParent } from 'utils/post-message';
import { getVoteMessage, VoteMessagesTypes } from './getVoteMessage';
import { getBlockingDurations } from './getBlockingDurations';
import { boundActions } from './connected-comment';
import { CommentVotes } from './comment-votes';
import styles from './comment.module.css';
import './styles';
@@ -61,19 +60,6 @@ export interface State {
renderDummy: boolean;
isCopied: boolean;
editDeadline: Date | null;
voteErrorMessage: string | null;
/**
* delta of the score:
* default is 0.
* if user upvoted delta will be incremented
* if downvoted delta will be decremented
*/
scoreDelta: number;
/**
* score copied from props, that updates instantly,
* without server response
*/
cachedScore: number;
initial: boolean;
}
@@ -82,11 +68,8 @@ export class Comment extends Component<CommentProps, State> {
/** comment text node. Used in comment text copying */
textNode = createRef<HTMLDivElement>();
updateState(props: CommentProps) {
const newState: Partial<State> = {
scoreDelta: props.data.vote,
cachedScore: props.data.score,
};
updateState = (props: CommentProps) => {
const newState: Partial<State> = {};
if (props.inView) {
newState.renderDummy = false;
@@ -106,15 +89,13 @@ export class Comment extends Component<CommentProps, State> {
}
return newState;
}
};
state = {
renderDummy: typeof this.props.inView === 'boolean' ? !this.props.inView : false,
isCopied: false,
editDeadline: null,
voteErrorMessage: null,
scoreDelta: 0,
cachedScore: this.props.data.score,
initial: true,
...this.updateState(this.props),
};
@@ -238,48 +219,6 @@ export class Comment extends Component<CommentProps, State> {
this.props.hideUser!(this.props.data.user);
};
handleVoteError = (e: FetcherError, originalScore: number, originalDelta: number) => {
this.setState({
scoreDelta: originalDelta,
cachedScore: originalScore,
voteErrorMessage: extractErrorMessageFromResponse(e, this.props.intl),
});
};
sendVotingRequest = (votingValue: number, originalScore: number, originalDelta: number) => {
this.votingPromise = this.votingPromise
.then(() => this.props.putCommentVote!(this.props.data.id, votingValue))
.catch((e) => this.handleVoteError(e, originalScore, originalDelta));
};
increaseScore = () => {
const { cachedScore, scoreDelta } = this.state;
if (scoreDelta === 1) return;
this.setState({
scoreDelta: scoreDelta + 1,
cachedScore: cachedScore + 1,
voteErrorMessage: null,
});
this.sendVotingRequest(1, cachedScore, scoreDelta);
};
decreaseScore = () => {
const { cachedScore, scoreDelta } = this.state;
if (scoreDelta === -1) return;
this.setState({
scoreDelta: scoreDelta - 1,
cachedScore: cachedScore - 1,
voteErrorMessage: null,
});
this.sendVotingRequest(-1, cachedScore, scoreDelta);
};
addComment = async (text: string, title: string, pid?: CommentType['id']) => {
await this.props.addComment!(text, title, pid);
@@ -311,6 +250,17 @@ export class Comment extends Component<CommentProps, State> {
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));
@@ -357,38 +307,6 @@ export class Comment extends Component<CommentProps, State> {
return this.props.data.user.id === this.props.user!.id;
};
/**
* returns reason for disabled downvoting
*/
getDownvoteDisabledReason = (): string | null => {
const intl = this.props.intl;
if (!(this.props.view === 'main' || this.props.view === 'pinned'))
return getVoteMessage(VoteMessagesTypes.ONLY_POST_PAGE, intl);
if (this.props.post_info?.read_only) return getVoteMessage(VoteMessagesTypes.READONLY, intl);
if (this.props.data.delete) return getVoteMessage(VoteMessagesTypes.DELETED, intl);
if (this.isCurrentUser()) return getVoteMessage(VoteMessagesTypes.OWN_COMMENT, intl);
if (StaticStore.config.positive_score && this.props.data.score < 1)
return getVoteMessage(VoteMessagesTypes.ONLY_POSITIVE, intl);
if (this.isGuest()) return getVoteMessage(VoteMessagesTypes.GUEST, intl);
if (this.isAnonymous() && !StaticStore.config.anon_vote) return getVoteMessage(VoteMessagesTypes.ANONYMOUS, intl);
return null;
};
/**
* returns reason for disabled upvoting
*/
getUpvoteDisabledReason = (): string | null => {
const intl = this.props.intl;
if (!(this.props.view === 'main' || this.props.view === 'pinned'))
return getVoteMessage(VoteMessagesTypes.ONLY_POST_PAGE, intl);
if (this.props.post_info?.read_only) return getVoteMessage(VoteMessagesTypes.READONLY, intl);
if (this.props.data.delete) return getVoteMessage(VoteMessagesTypes.DELETED, intl);
if (this.isCurrentUser()) return getVoteMessage(VoteMessagesTypes.OWN_COMMENT, intl);
if (this.isGuest()) return getVoteMessage(VoteMessagesTypes.GUEST, intl);
if (this.isAnonymous() && !StaticStore.config.anon_vote) return getVoteMessage(VoteMessagesTypes.ANONYMOUS, intl);
return null;
};
getCommentControls = (): JSX.Element[] => {
const isAdmin = this.isAdmin();
const isCurrentUser = this.isCurrentUser();
@@ -476,13 +394,7 @@ export class Comment extends Component<CommentProps, State> {
const isReplying = props.editMode === CommentMode.Reply;
const isEditing = props.editMode === CommentMode.Edit;
const lowCommentScore = StaticStore.config.low_score;
const downvotingDisabledReason = this.getDownvoteDisabledReason();
const isDownvotingDisabled = downvotingDisabledReason !== null;
const upvotingDisabledReason = this.getUpvoteDisabledReason();
const isUpvotingDisabled = upvotingDisabledReason !== null;
const editable = props.repliesCount === 0 && state.editDeadline;
const scoreSignEnabled = !StaticStore.config.positive_score;
const uploadImageHandler = this.isAnonymous() ? undefined : this.props.uploadImage;
const commentControls = this.getCommentControls();
const intl = props.intl;
@@ -494,9 +406,6 @@ export class Comment extends Component<CommentProps, State> {
const o = {
...props.data,
controversyText: intl.formatMessage(messages.controversy, {
value: (props.data.controversy || 0).toFixed(2),
}),
text:
props.view === 'preview'
? getTextSnippet(props.data.text)
@@ -512,22 +421,12 @@ export class Comment extends Component<CommentProps, State> {
return span.innerText;
})
: props.data.orig,
score: {
value: Math.abs(state.cachedScore),
sign: !scoreSignEnabled ? '' : state.cachedScore > 0 ? '+' : state.cachedScore < 0 ? '' : null,
view: state.cachedScore > 0 ? 'positive' : state.cachedScore < 0 ? 'negative' : undefined,
},
user: props.data.user,
};
const defaultMods = {
disabled: props.disabled,
pinned: props.data.pin,
// TODO: we also have critical_score, so we need to collapse comments with it in future
useless:
!!props.isUserBanned ||
!!props.data.delete ||
(props.view !== 'preview' && props.data.score < lowCommentScore && !props.data.pin && !props.disabled),
// TODO: add default view mod or don't?
guest: isGuest,
view: props.view === 'main' || props.view === 'pinned' ? props.data.user.admin && 'admin' : props.view,
@@ -661,51 +560,17 @@ export class Comment extends Component<CommentProps, State> {
<FormattedMessage id="comment.deleted-user" defaultMessage="Deleted" />
</span>
)}
<span className={b('comment__score', {}, { view: o.score.view })}>
<span
className={b(
'comment__vote',
{},
{ type: 'up', selected: state.scoreDelta === 1, disabled: isUpvotingDisabled }
)}
aria-disabled={state.scoreDelta === 1 || isUpvotingDisabled ? 'true' : 'false'}
{...getHandleClickProps(isUpvotingDisabled ? undefined : this.increaseScore)}
title={upvotingDisabledReason || undefined}
>
Vote up
</span>
<span className="comment__score-value" title={o.controversyText}>
{o.score.sign}
{o.score.value}
</span>
<span
className={b(
'comment__vote',
{},
{ type: 'down', selected: state.scoreDelta === -1, disabled: isDownvotingDisabled }
)}
aria-disabled={state.scoreDelta === -1 || isUpvotingDisabled ? 'true' : 'false'}
{...getHandleClickProps(isDownvotingDisabled ? undefined : this.decreaseScore)}
title={downvotingDisabledReason || undefined}
>
Vote down
</span>
</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">
{!!state.voteErrorMessage && (
<div className="voting__error" role="alert">
<FormattedMessage
id="comment.vote-error"
defaultMessage="Voting error: {voteErrorMessage}"
values={{ voteErrorMessage: state.voteErrorMessage }}
/>
</div>
)}
{(!props.collapsed || props.view === 'pinned') && (
<div
className={b('comment__text', { mix: b('raw-content', {}, { theme: props.theme }) })}
@@ -868,10 +733,7 @@ const messages = defineMessages({
id: 'comment.deleted-comment',
defaultMessage: 'This comment was deleted',
},
controversy: {
id: 'comment.controversy',
defaultMessage: 'Controversy: {value}',
},
toggleVerification: {
id: 'comment.toggle-verification',
defaultMessage: 'Toggle verification',
@@ -12,7 +12,7 @@ import { Comment as CommentType } from 'common/types';
import { useStore } from 'react-redux';
import { StoreState } from 'store';
import { addComment, removeComment, updateComment, setPinState, putVote, setCommentMode } from 'store/comments/actions';
import { addComment, removeComment, updateComment, setPinState, setCommentMode } from 'store/comments/actions';
import { blockUser, unblockUser, hideUser, setVerifiedStatus } from 'store/user/actions';
import { Comment, CommentProps } from './comment';
@@ -57,7 +57,6 @@ export const boundActions = bindActions({
removeComment,
setReplyEditState: setCommentMode,
setPinState,
putCommentVote: putVote,
blockUser,
unblockUser,
hideUser,
@@ -1,55 +0,0 @@
import { IntlShape, defineMessages } from 'react-intl';
const voteMessages = defineMessages({
ownComment: {
id: 'vote.own-comment',
defaultMessage: `Can't vote for your own comment`,
},
guest: {
id: 'vote.guest',
defaultMessage: 'Sign in to vote',
},
onlyPostPage: {
id: 'vote.only-post-page',
defaultMessage: `Voting allowed only on post's page`,
},
readonly: {
id: 'vote.readonly',
defaultMessage: `Can't vote on read-only topics`,
},
deleted: {
id: 'vote.deleted',
defaultMessage: `Can't vote for deleted comment`,
},
anonymous: {
id: 'vote.anonymous',
defaultMessage: `Anonymous users can't vote`,
},
onlyPositive: {
id: 'vote.only-positive',
defaultMessage: `Only positive score allowed`,
},
});
export enum VoteMessagesTypes {
OWN_COMMENT,
GUEST,
ONLY_POST_PAGE,
READONLY,
DELETED,
ANONYMOUS,
ONLY_POSITIVE,
}
export function getVoteMessage(type: VoteMessagesTypes, intl: IntlShape) {
const messages = {
[VoteMessagesTypes.OWN_COMMENT]: intl.formatMessage(voteMessages.ownComment),
[VoteMessagesTypes.GUEST]: intl.formatMessage(voteMessages.guest),
[VoteMessagesTypes.ONLY_POST_PAGE]: intl.formatMessage(voteMessages.onlyPostPage),
[VoteMessagesTypes.READONLY]: intl.formatMessage(voteMessages.readonly),
[VoteMessagesTypes.DELETED]: intl.formatMessage(voteMessages.deleted),
[VoteMessagesTypes.ANONYMOUS]: intl.formatMessage(voteMessages.anonymous),
[VoteMessagesTypes.ONLY_POSITIVE]: intl.formatMessage(voteMessages.onlyPositive),
};
return messages[type];
}
@@ -18,20 +18,12 @@ import './__controls/comment__controls.css';
import './__info/comment__info.css';
import './__input/comment__input.css';
import './__link-to-parent/comment__link-to-parent.css';
import './__score/comment__score.css';
import './__score-value/comment__score-value.css';
import './__status/comment__status.css';
import './__text/comment__text.css';
import './__time/comment__time.css';
import './__user-id/comment__user-id.css';
import './__username/comment__username.css';
import './__vote/comment__vote.css';
import './__vote/_disabled/comment__vote_disabled.css';
import './__vote/_selected/comment__vote_selected.css';
import './__vote/_type/_down/comment__vote_type_down.css';
import './__vote/_type/_up/comment__vote_type_up.css';
import './_collapsed/comment_collapsed.css';
import './_editing/comment_editing.css';
import './_replying/comment_replying.css';
@@ -0,0 +1,18 @@
import { h } from 'preact';
import '@testing-library/jest-dom';
import { screen } from '@testing-library/preact';
import { render } from 'tests/utils';
import { ArrowIcon } from './arrow';
describe('<ArrowIcon />', () => {
it('should be rendered with default size', async () => {
render(<ArrowIcon title="icon" />);
expect(await screen.findByTitle('icon')).toHaveAttribute('width', '14');
expect(await screen.findByTitle('icon')).toHaveAttribute('height', '14');
});
it('should be rendered with provided size', async () => {
render(<ArrowIcon title="icon" size={16} />);
expect(await screen.findByTitle('icon')).toHaveAttribute('width', '16');
expect(await screen.findByTitle('icon')).toHaveAttribute('height', '16');
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
import { h, JSX } from 'preact';
type Props = { size?: number } & JSX.HTMLAttributes<SVGSVGElement>;
type Props = JSX.HTMLAttributes<SVGSVGElement> & { size?: number };
export function ArrowIcon({ size = 14, ...props }: Props) {
return (
+1
View File
@@ -0,0 +1 @@
export * from './tooltip';
@@ -0,0 +1,53 @@
.root {
position: relative;
}
.tooltip {
position: absolute;
visibility: hidden;
padding: 4px 6px;
border-radius: 4px;
background-color: rgb(var(--black-color));
color: rgb(var(--white-color));
}
.tooltip::after {
position: absolute;
content: '';
border: 6px solid var(--transparent);
}
.root:hover .tooltip {
visibility: visible;
}
.tooltipPermanent {
visibility: visible;
}
.rootVisible {
opacity: 1;
visibility: visible;
}
.permanent {
display: block;
}
.top-left {
margin-bottom: 10px;
bottom: 100%;
right: 0;
&::after {
top: 100%;
right: 10px;
border-top-color: rgb(var(--black-color));
border-bottom-width: 0;
}
}
.top-right {
bottom: 100%;
left: 0;
}
@@ -0,0 +1,32 @@
import { h } from 'preact';
import '@testing-library/jest-dom';
import { render } from 'tests/utils';
import { Tooltip } from './tooltip';
import { screen } from '@testing-library/preact';
describe('<Tooltip />', () => {
it('should not render tooltip without content', () => {
render(<Tooltip position="top-left">Hello</Tooltip>);
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
it('should not render tooltip with content', () => {
render(
<Tooltip position="top-left" content="Howdy">
Hello
</Tooltip>
);
expect(screen.queryByRole('tooltip')).toBeInTheDocument();
expect(screen.getByText('Howdy')).toBeInTheDocument();
});
it.each([['top-left'], ['top-right']] as ['top-left' | 'top-right'][])('should render tooltip on %s', (position) => {
render(
<Tooltip position={position} content="Howdy">
Hello
</Tooltip>
);
expect(screen.queryByRole('tooltip')).toHaveClass(position);
});
});
@@ -0,0 +1,47 @@
import { h } from 'preact';
import clsx from 'clsx';
import styles from './tooltip.module.css';
import { useEffect } from 'preact/hooks';
type Props = {
children: preact.ComponentChild;
className?: string;
content?: preact.ComponentChild;
permanent?: boolean;
position: 'top-left' | 'top-right';
hideBehavior?: 'mouseleave' | 'click';
hideTimeout?: number;
onHide?(): void;
};
export function Tooltip({
className,
children,
content,
permanent,
hideBehavior,
hideTimeout,
position,
onHide,
}: Props) {
useEffect(() => {
if (content && hideTimeout && onHide) {
setTimeout(onHide, hideTimeout);
}
}, [content, hideTimeout, onHide]);
return (
<div className={clsx(styles.root, className)}>
{content && (
<div
role="tooltip"
className={clsx(styles.tooltip, permanent && styles.tooltipPermanent, styles[position])}
onMouseLeave={hideBehavior === 'mouseleave' ? onHide : undefined}
>
{content}
</div>
)}
{children}
</div>
);
}
+172 -177
View File
@@ -1,177 +1,172 @@
{
"auth.back": "رجوع",
"auth.email-address": "البريد الإلكتروني",
"auth.loading": "جارٍ التحميل...",
"auth.oauth-button": "سجل دخولاً عبر {provider}",
"auth.oauth-source": "عبر التواصل الاجتماعي",
"auth.open-profile": "الصفحة الشخصية",
"auth.or": "أو",
"auth.signin": "دخول",
"auth.signout": "خروج",
"auth.submit": "إرسال",
"auth.symbols-restriction": "اسم المستخدم ينبغي أن يحتوي فقط على حروف، أرقام، مسافات، أو شرطات",
"auth.telegram-check": "تأكد",
"auth.telegram-link": "عبر الرابط",
"auth.telegram-message-1": "افتح تيليجرام",
"auth.telegram-message-2": "انقر \"ابدأ\" هناك.",
"auth.telegram-message-3": "ثم انقر \"تأكد\" أسفله.",
"auth.telegram-optional-qr": "أو عبر مسح رمز QR",
"auth.telegram-qr": "رمز QR للتيليجرام",
"auth.user-not-found": "لا مستخدم بهذا الاسم",
"auth.username": "اسم المستخدم",
"authPanel.disable-comments": "عطّل التعليقات",
"authPanel.disabled-cookies": "ألغِ حظرَ كوكيز الطرف الثالث للدخول أو أنشئ تعليقاً في",
"authPanel.enable-comments": "فعل التعليقات",
"authPanel.enable-cookies": "اسمح بالكوكيز للدخول والتعليق",
"authPanel.hide-settings": "أخفِ الإعدادات",
"authPanel.new-page": "صفحة جديدة",
"authPanel.read-only": "للقراءة فقط",
"authPanel.show-settings": "أظهر الإعدادات",
"blockingDuration.day": "ليومٍ",
"blockingDuration.month": "لشهرٍ",
"blockingDuration.permanently": "دائماً",
"blockingDuration.week": "لأسبوعٍ",
"comment.block": "حظر",
"comment.block-user": "هل تودُّ حظر {userName} لمدة {duration}؟",
"comment.blocked-user": "محظور",
"comment.blocking-period": "فترة الحظر",
"comment.cancel": "إلغاء",
"comment.controversy": "مستوى النقاش: {value}",
"comment.copied": ُسِخ!",
"comment.copy": "نسخ",
"comment.delete": "حذف",
"comment.delete-message": "هل تود حذف هذا التعليق؟",
"comment.deleted-comment": "هذا التعليق محذوف",
"comment.deleted-user": "حُذِف",
"comment.edit": "عدّل",
"comment.expired-time": "انتهى وقت التعديل",
"comment.go-to-parent": "اذهب للتعليق الأصلي",
"comment.hide": "أخفِ",
"comment.hide-user-comment": "هل تود إخفاء تعليقات {userName}؟",
"comment.pin": "ثبّت",
"comment.pin-comment": "هل تودُّ تثبيت هذا التعليق؟",
"comment.reply": "ردّ",
"comment.time": "{day} في {time}",
"comment.toggle-verification": "فعّل التحقق",
"comment.unblock": "ألغِ حظر",
"comment.unblock-user": "هل تود إلغاء حظر هذا المستخدم؟",
"comment.unpin": "إلغاء تثبيت",
"comment.unpin-comment": "هل تودُّ إلغاء تثبيت هذا التعليق؟",
"comment.unverified-user": "مستخدم غير مُحقَّق",
"comment.unverify-user": "هل تودُّ إلغاء التحقق من {userName}؟",
"comment.verified-user": "مستخدم مُحَقَّق",
"comment.verify-user": "هل تود تحقيق {userName}؟",
"comment.vote-error": "خطأ تصويت: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "رفع الصور مُعطّل للمجهولين. فضلاً سجل الدخول لإرفاق صور.",
"commentForm.exceeded-size": "{fileName} يتجاوز حد الرفع: {maxImageSize}",
"commentForm.input-placeholder": "تعليقك هنا",
"commentForm.new-comment": "تعليق جديد",
"commentForm.notice-about-styling": "تنميط الكتابة <a>بالماركداون</a> مدعوم",
"commentForm.preview": "عاين",
"commentForm.reply": "رُد",
"commentForm.save": "احفظ",
"commentForm.send": رسل",
"commentForm.subscribe-by": "اشترك عبر",
"commentForm.subscribe-or": "أو",
"commentForm.unauthorized-uploading-disabled": "رفع الصور مُعطّل لغير المُتحقق منهم. عليك بالدخول قبل الرفع.",
"commentForm.unexpected-error": "ثمّة عطلٌ ما. فضلاً عاود المحاولة لاحقاً.",
"commentForm.upload-file-fail": "رفع {fileName} فشل بخطأ: \"{errorMessage}",
"commentForm.uploading": "جارٍ الرفع...",
"commentForm.uploading-file": "جارٍ رفع {fileName}...",
"commentsSort.best": "الأفضل",
"commentsSort.least-controversial": "الأقل جدلاً",
"commentsSort.least-recently-updated": "الأقدم تحديثاً",
"commentsSort.most-controversial": "الأكثر جدلاً",
"commentsSort.newest": "الأجدد",
"commentsSort.oldest": "الأقدم",
"commentsSort.recently-updated": "الأجدد تحديثاً",
"commentsSort.worst": "الأسوأ",
"empty-state": "لا تعليقات للآن",
"errors.0": "ثمّة عطلٌ ما. فضلاً عاود المحاولة لاحقاً.",
"errors.1": ا يمكن إيجاد التعليق. فضلاً حدث الصفحة وعاود المحاولة.",
"errors.10": "وقت تعديل التعليق فات.",
"errors.11": لتعليق ردُّ بالفعل. لا يمكنك التعديل.",
"errors.12": ا يُمكن حفظ نتيجة التصويت. فضلاً عاود المحاولة لاحقاً.",
"errors.13": "لا يمكنك التصويت لتعليقك.",
"errors.14": "لقد صوّتَّ بالفعل للتعليق",
"errors.15": "تصويتات فوق المسموح للتعليق.",
"errors.16": "التعليق وصل لأقل مستوى.",
"errors.17": "رُفض الإجراء. فضلاً عاود المحاولة لاحقاً.",
"errors.18": "الملف المطلوب لا يمكن إيجاده",
"errors.19": "التعليق يحتوي على كلمات محظورة.",
"errors.2": "خطأ في معالجة الطلب القادم.",
"errors.20": "الصورة المنشورة غير موجودة. فضلاً عاود رفعها.",
"errors.3": "لا صلاحية لك في هذا الإجراء.",
"errors.4": "محتويات التعليق غير صالحة",
"errors.5": "التعليق لا يمكن إيجاده. فضلاً عاود تحميل الصفحة.",
"errors.6": "الموقع لا يمكن إيجاده. فضلاً عاود تحميل الصفحة",
"errors.7": "المستخدم محظور",
"errors.8": "لا يمكن نشر تعليقات على هذه الصفحة. التعليقات مؤرشفة للقراءة فقط.",
"errors.9": "فشل تعديل التعليق. فضلاً، عاود المحاولة.",
"errors.failed-fetch": "فشل الاتصال. فضلاً تأكد من اتصالك بالإنترنت.",
"errors.forbidden": "ممنوع.",
"errors.not-authorized": "غير مخوّل بالدخول.",
"errors.to-many-request": "لقد وصلت للحد الأقصى من الطلبات.",
"errors.unexpected-error": "ثمة عطلٌ ما.",
"profile.close": "أغلق الملف.",
"profile.request-to-delete-data": "طلب حذف بياناتي",
"retry": "إعادة المحاولة",
"root.pinned-comments": "التعليقات المثبتة",
"root.powered-by": "صنع باستخدام <a>Remark42</a>",
"root.show-more": "أظهر المزيد",
"settings.block": "حظر",
"settings.block-time": "إلى {day} في {time}",
"settings.block-user": "هل تودُّ حظر {userName}؟",
"settings.blocked-users-header": "المحظورون:",
"settings.blocked-users-title": "المحظورون",
"settings.hidden-user-header": "المخفيون:",
"settings.hidden-users-title": "المخفيون",
"settings.hide": "أخفِ",
"settings.no-blocked-users": "لا يوجد محظورون",
"settings.no-hidden-users": "لا يوجد مخفيون",
"settings.permanently": "دائماً",
"settings.show": "أظهر",
"settings.unblock": "إلغِ حظر",
"settings.unblock-user": "هل تودُّ إلغاء حظر {userName}؟",
"settings.unknown": "غير معروف",
"sort-by": "صنّف حسب",
"subscribeByEmail.back": "عودة",
"subscribeByEmail.close": "أغلق",
"subscribeByEmail.email": "البريد الإلكتروني",
"subscribeByEmail.have-been-subscribed": "لقد اشتركت في تحديثات البريد الإلكتروني",
"subscribeByEmail.have-been-unsubscribed": "لقد ألغيت اشتراكك في تحديثات البريد الإلكتروني",
"subscribeByEmail.only-registered-users": "متاح للمُسجّلين فقط",
"subscribeByEmail.submit": "أرسل",
"subscribeByEmail.subscribe": "اشترك",
"subscribeByEmail.subscribe-by-email": "اشترك عبر البريد",
"subscribeByEmail.subscribe-to-replies": "اشترك للردود",
"subscribeByEmail.subscribed": "أنت مشترك بالتحديثات عبر البريد",
"subscribeByEmail.unsubscribe": "إلغاء الاشتراك",
"subscribeByRSS.button-title": "اشترك عبر RSS",
"subscribeByRSS.replies": "الردود",
"subscribeByRSS.site": "الموقع",
"subscribeByRSS.thread": "سلسلة ردود",
"subscribeByRSS.title": "RSS",
"token": "رمز",
"token.expired": "الرمز انتهت صلاحيته",
"token.invalid": "الرمز غير صالح",
"toolbar.attach-image": "أرفق صورةً، أو اسحبها وأسقطها، أو الصقها من الحافظة",
"toolbar.bold": "أضف نصّاً عريضاً {shortcut}",
"toolbar.code": درج نصّاً برمجياً",
"toolbar.header": "أضف عنواناً",
"toolbar.italic": "أضف نصاً مائلاً {shortcut}",
"toolbar.link": "أضف رابطاً {shortcut}",
"toolbar.ordered-list": "أضف قائمةً مرقمةً",
"toolbar.quote": "أضف اقتباساً",
"toolbar.unordered-list": "أضف قائمةً مُنقّطةً",
"user.comments": "تعليقات",
"user.load-more": "تحميل المزيد",
"user.my-comments": "تعليقات",
"vote.anonymous": "لا يُسمح بتصويت المجهولين",
"vote.deleted": "لا يمكن التصويت لتعليقٍ محذوف",
"vote.guest": "سجل الدخول للتصويت",
"vote.only-positive": "مسموح بنقاطٍ موجبة فقط",
"vote.only-post-page": "التصويت مسموح على صفحة المنشور فقط",
"vote.own-comment": "لا يمكنك التصويت لتعليقك.",
"vote.readonly": "لا يمكنك التصويت على مواضيع للقراءة فقط."
}
{
"auth.back": "رجوع",
"auth.email-address": "البريد الإلكتروني",
"auth.loading": "جارٍ التحميل...",
"auth.oauth-button": "سجل دخولاً عبر {provider}",
"auth.oauth-source": "عبر التواصل الاجتماعي",
"auth.open-profile": "الصفحة الشخصية",
"auth.or": "أو",
"auth.signin": "دخول",
"auth.signout": "خروج",
"auth.submit": "إرسال",
"auth.symbols-restriction": "اسم المستخدم ينبغي أن يحتوي فقط على حروف، أرقام، مسافات، أو شرطات",
"auth.telegram-check": "تأكد",
"auth.telegram-link": "عبر الرابط",
"auth.telegram-message-1": "افتح تيليجرام",
"auth.telegram-message-2": "انقر \"ابدأ\" هناك.",
"auth.telegram-message-3": "ثم انقر \"تأكد\" أسفله.",
"auth.telegram-optional-qr": "أو عبر مسح رمز QR",
"auth.telegram-qr": "رمز QR للتيليجرام",
"auth.user-not-found": "لا مستخدم بهذا الاسم",
"auth.username": "اسم المستخدم",
"authPanel.disable-comments": "عطّل التعليقات",
"authPanel.disabled-cookies": "ألغِ حظرَ كوكيز الطرف الثالث للدخول أو أنشئ تعليقاً في",
"authPanel.enable-comments": "فعل التعليقات",
"authPanel.enable-cookies": "اسمح بالكوكيز للدخول والتعليق",
"authPanel.hide-settings": "أخفِ الإعدادات",
"authPanel.new-page": "صفحة جديدة",
"authPanel.read-only": "للقراءة فقط",
"authPanel.show-settings": "أظهر الإعدادات",
"blockingDuration.day": "ليومٍ",
"blockingDuration.month": "لشهرٍ",
"blockingDuration.permanently": "دائماً",
"blockingDuration.week": "لأسبوعٍ",
"comment.block": "حظر",
"comment.block-user": "هل تودُّ حظر {userName} لمدة {duration}؟",
"comment.blocked-user": "محظور",
"comment.blocking-period": "فترة الحظر",
"comment.cancel": "إلغاء",
"comment.copied": "نُسِخ!",
"comment.copy": سخ",
"comment.delete": "حذف",
"comment.delete-message": "هل تود حذف هذا التعليق؟",
"comment.deleted-comment": "هذا التعليق محذوف",
"comment.deleted-user": "حُذِف",
"comment.edit": "عدّل",
"comment.expired-time": "انتهى وقت التعديل",
"comment.go-to-parent": "اذهب للتعليق الأصلي",
"comment.hide": "أخفِ",
"comment.hide-user-comment": "هل تود إخفاء تعليقات {userName}؟",
"comment.pin": "ثبّت",
"comment.pin-comment": "هل تودُّ تثبيت هذا التعليق؟",
"comment.reply": "ردّ",
"comment.time": "{day} في {time}",
"comment.toggle-verification": "فعّل التحقق",
"comment.unblock": "ألغِ حظر",
"comment.unblock-user": "هل تود إلغاء حظر هذا المستخدم؟",
"comment.unpin": "إلغاء تثبيت",
"comment.unpin-comment": "هل تودُّ إلغاء تثبيت هذا التعليق؟",
"comment.unverified-user": "مستخدم غير مُحقَّق",
"comment.unverify-user": "هل تودُّ إلغاء التحقق من {userName}؟",
"comment.verified-user": "مستخدم مُحَقَّق",
"comment.verify-user": "هل تود تحقيق {userName}؟",
"commentForm.anonymous-uploading-disabled": "رفع الصور مُعطّل للمجهولين. فضلاً سجل الدخول لإرفاق صور.",
"commentForm.exceeded-size": "{fileName} يتجاوز حد الرفع: {maxImageSize}",
"commentForm.input-placeholder": "تعليقك هنا",
"commentForm.new-comment": "تعليق جديد",
"commentForm.notice-about-styling": "تنميط الكتابة <a>بالماركداون</a> مدعوم",
"commentForm.preview": "عاين",
"commentForm.reply": "رُد",
"commentForm.save": "احفظ",
"commentForm.send": "أرسل",
"commentForm.subscribe-by": "اشترك عبر",
"commentForm.subscribe-or": و",
"commentForm.unauthorized-uploading-disabled": "رفع الصور مُعطّل لغير المُتحقق منهم. عليك بالدخول قبل الرفع.",
"commentForm.unexpected-error": "ثمّة عطلٌ ما. فضلاً عاود المحاولة لاحقاً.",
"commentForm.upload-file-fail": "رفع {fileName} فشل بخطأ: \"{errorMessage}",
"commentForm.uploading": "جارٍ الرفع...",
"commentForm.uploading-file": "جارٍ رفع {fileName}...",
"commentsSort.best": "الأفضل",
"commentsSort.least-controversial": "الأقل جدلاً",
"commentsSort.least-recently-updated": "الأقدم تحديثاً",
"commentsSort.most-controversial": "الأكثر جدلاً",
"commentsSort.newest": "الأجدد",
"commentsSort.oldest": "الأقدم",
"commentsSort.recently-updated": "الأجدد تحديثاً",
"commentsSort.worst": "الأسوأ",
"empty-state": "لا تعليقات للآن",
"errors.0": "ثمّة عطلٌ ما. فضلاً عاود المحاولة لاحقاً.",
"errors.1": "لا يمكن إيجاد التعليق. فضلاً حدث الصفحة وعاود المحاولة.",
"errors.10": "وقت تعديل التعليق فات.",
"errors.11": لتعليق ردُّ بالفعل. لا يمكنك التعديل.",
"errors.12": "لا يُمكن حفظ نتيجة التصويت. فضلاً عاود المحاولة لاحقاً.",
"errors.13": ا يمكنك التصويت لتعليقك.",
"errors.14": قد صوّتَّ بالفعل للتعليق",
"errors.15": "تصويتات فوق المسموح للتعليق.",
"errors.16": "التعليق وصل لأقل مستوى.",
"errors.17": "رُفض الإجراء. فضلاً عاود المحاولة لاحقاً.",
"errors.18": "الملف المطلوب لا يمكن إيجاده",
"errors.19": "التعليق يحتوي على كلمات محظورة.",
"errors.2": "خطأ في معالجة الطلب القادم.",
"errors.20": "الصورة المنشورة غير موجودة. فضلاً عاود رفعها.",
"errors.3": "لا صلاحية لك في هذا الإجراء.",
"errors.4": "محتويات التعليق غير صالحة",
"errors.5": "التعليق لا يمكن إيجاده. فضلاً عاود تحميل الصفحة.",
"errors.6": "الموقع لا يمكن إيجاده. فضلاً عاود تحميل الصفحة",
"errors.7": "المستخدم محظور",
"errors.8": "لا يمكن نشر تعليقات على هذه الصفحة. التعليقات مؤرشفة للقراءة فقط.",
"errors.9": "فشل تعديل التعليق. فضلاً، عاود المحاولة.",
"errors.failed-fetch": "فشل الاتصال. فضلاً تأكد من اتصالك بالإنترنت.",
"errors.forbidden": "ممنوع.",
"errors.not-authorized": "غير مخوّل بالدخول.",
"errors.to-many-request": "لقد وصلت للحد الأقصى من الطلبات.",
"errors.unexpected-error": "ثمة عطلٌ ما.",
"profile.close": "أغلق الملف.",
"profile.request-to-delete-data": "طلب حذف بياناتي",
"retry": "إعادة المحاولة",
"root.pinned-comments": "التعليقات المثبتة",
"root.powered-by": "صنع باستخدام <a>Remark42</a>",
"root.show-more": "أظهر المزيد",
"settings.block": "حظر",
"settings.block-time": "إلى {day} في {time}",
"settings.block-user": "هل تودُّ حظر {userName}؟",
"settings.blocked-users-header": "المحظورون:",
"settings.blocked-users-title": "المحظورون",
"settings.hidden-user-header": "المخفيون:",
"settings.hidden-users-title": "المخفيون",
"settings.hide": "أخفِ",
"settings.no-blocked-users": "لا يوجد محظورون",
"settings.no-hidden-users": "لا يوجد مخفيون",
"settings.permanently": "دائماً",
"settings.show": "أظهر",
"settings.unblock": "إلغِ حظر",
"settings.unblock-user": "هل تودُّ إلغاء حظر {userName}؟",
"settings.unknown": "غير معروف",
"sort-by": "صنّف حسب",
"subscribeByEmail.back": "عودة",
"subscribeByEmail.close": "أغلق",
"subscribeByEmail.email": "البريد الإلكتروني",
"subscribeByEmail.have-been-subscribed": "لقد اشتركت في تحديثات البريد الإلكتروني",
"subscribeByEmail.have-been-unsubscribed": "لقد ألغيت اشتراكك في تحديثات البريد الإلكتروني",
"subscribeByEmail.only-registered-users": "متاح للمُسجّلين فقط",
"subscribeByEmail.submit": "أرسل",
"subscribeByEmail.subscribe": "اشترك",
"subscribeByEmail.subscribe-by-email": "اشترك عبر البريد",
"subscribeByEmail.subscribe-to-replies": "اشترك للردود",
"subscribeByEmail.subscribed": "أنت مشترك بالتحديثات عبر البريد",
"subscribeByEmail.unsubscribe": "إلغاء الاشتراك",
"subscribeByRSS.button-title": "اشترك عبر RSS",
"subscribeByRSS.replies": "الردود",
"subscribeByRSS.site": "الموقع",
"subscribeByRSS.thread": "سلسلة ردود",
"subscribeByRSS.title": "RSS",
"token": "رمز",
"token.expired": "الرمز انتهت صلاحيته",
"token.invalid": "الرمز غير صالح",
"toolbar.attach-image": "أرفق صورةً، أو اسحبها وأسقطها، أو الصقها من الحافظة",
"toolbar.bold": "أضف نصّاً عريضاً {shortcut}",
"toolbar.code": "أدرج نصّاً برمجياً",
"toolbar.header": "أضف عنواناً",
"toolbar.italic": ضف نصاً مائلاً {shortcut}",
"toolbar.link": "أضف رابطاً {shortcut}",
"toolbar.ordered-list": "أضف قائمةً مرقمةً",
"toolbar.quote": "أضف اقتباساً",
"toolbar.unordered-list": "أضف قائمةً مُنقّطةً",
"user.comments": "تعليقات",
"user.load-more": "تحميل المزيد",
"user.my-comments": "تعليقات",
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Заблакаваны",
"comment.blocking-period": "Перыяд блакавання",
"comment.cancel": "Скасаваць",
"comment.controversy": "Спрэчнасць: {value}",
"comment.copied": "Скапіявана!",
"comment.copy": "Капіяваць",
"comment.delete": "Выдаліць",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Прыбраць статус сапраўднасці ўліковага запісу {userName}?",
"comment.verified-user": "Спраўджаны ўліковы запіс",
"comment.verify-user": "Пацвердзіць сапраўднасць уліковага запісу {userName}?",
"comment.vote-error": "Памылка галасавання: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Загрузка выяў забароненая для ананімных карыстальнікаў. Калі ласка, увайдзіце як не ананімны карыстальнік, каб прымацоўваць выявы.",
"commentForm.exceeded-size": "Памер файла {fileName} мусіць быць меншым за {maxImageSize}",
"commentForm.input-placeholder": "Напісаць каментар",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Ананімныя карыстальнікі не могуць галасаваць",
"vote.deleted": "Нельга галасаваць за выдалены каментар",
"vote.guest": "Увайдзіце ў сістэму, каб галасаваць",
"vote.only-positive": "Дазволеныя толькі станоўчыя адзнакі",
"vote.only-post-page": "Галасаваць можна толькі за артыкулы",
"vote.own-comment": "Вы не можаце галасаваць за свае каментары",
"vote.readonly": "Нельга галасаваць за каментары, што ў рэжыме толькі для чытання"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Блокиран",
"comment.blocking-period": "Период за блокиране",
"comment.cancel": "Отказ",
"comment.controversy": "Полемика: {value}",
"comment.copied": "Копирано!",
"comment.copy": "Копирай",
"comment.delete": "Изтрий",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Искате ли да маркирате {userName} като непроверен?",
"comment.verified-user": "Проверен потребител",
"comment.verify-user": "Искате ли да маркирате {userName} като проверен?",
"comment.vote-error": "Грешка при гласуване: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Анонимни потребители не могат да качват картинки. Моля влезте като потребител за да добавите картини.",
"commentForm.exceeded-size": "{fileName} е по-голям от лимита от {maxImageSize}",
"commentForm.input-placeholder": "Вашият коментар тук",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Анонимни потребители не могат да гласуват",
"vote.deleted": "Не може да гласъвате за изтрит коментарт",
"vote.guest": "Влезте за да гласувате",
"vote.only-positive": "Само положителна оценка е позволена",
"vote.only-post-page": "Гласуването е разрешено само на страницата на бележката",
"vote.own-comment": "Не може да гласувате за ваш коментар",
"vote.readonly": "Не може да гласувате за нишки маркирани само за четене"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Bloqueado",
"comment.blocking-period": "Período de bloqueio",
"comment.cancel": "Cancelar",
"comment.controversy": "Controvérsia: {value}",
"comment.copied": "Copiado!",
"comment.copy": "Copiar",
"comment.delete": "Excluir",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Quer cancelar a verificação de {userName}?",
"comment.verified-user": "Usuário verificado",
"comment.verify-user": "Deseja verificar {userName}?",
"comment.vote-error": "Erro de votação: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "O upload de imagens está desabilitado para usuários anônimos. Não faça login como um usuário anônimo para poder anexar imagens.",
"commentForm.exceeded-size": "{fileName} excede o limite de tamanho de {maxImageSize}",
"commentForm.input-placeholder": "Seu comentário aqui",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Usuários anônimos não podem votar",
"vote.deleted": "Não posso votar em um comentário excluído",
"vote.guest": "Faça login para votar",
"vote.only-positive": "Somente pontuação positiva permitida",
"vote.only-post-page": "Votação permitida apenas na página da postagem",
"vote.own-comment": "Não é possível votar no seu próprio comentário",
"vote.readonly": "Não é possível votar em tópicos somente leitura"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Gesperrt",
"comment.blocking-period": "Sperrdauer",
"comment.cancel": "Abbrechen",
"comment.controversy": "Strittigkeit: {value}",
"comment.copied": "Kopiert!",
"comment.copy": "Kopieren",
"comment.delete": "Löschen",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Möchten Sie den Benutzer {userName} wirklich als unbestätigt speichern?",
"comment.verified-user": "Bestätigter Benutzer",
"comment.verify-user": "Möchten Sie den Benutzer {userName} wirklich bestätigen?",
"comment.vote-error": "Fehler beim Abstimmen: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Das Hochladen von Bildern ist für anonyme Benutzer deaktiviert. Bitte melden Sie sich an, um Bilder hochladen zu können.",
"commentForm.exceeded-size": "Die Datei {fileName} überschreitet das Größenlimit von {maxImageSize}",
"commentForm.input-placeholder": "Geben Sie hier Ihren Kommentar ein",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Anonyme Benutzer können leider nicht abstimmen",
"vote.deleted": "Sie können nicht für einen gelöschten Kommentar abstimmen",
"vote.guest": "Melden Sie sich an, um abzustimmen",
"vote.only-positive": "Nur positive Punktzahl erlaubt",
"vote.only-post-page": "Die Abstimmung ist nur auf der Artikelseite erlaubt",
"vote.own-comment": "Sie können nicht für Ihren eigenen Kommentar abstimmen",
"vote.readonly": "Sie können nicht bei schreibgeschützten Themen abstimmen"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Blocked",
"comment.blocking-period": "Blocking period",
"comment.cancel": "Cancel",
"comment.controversy": "Controversy: {value}",
"comment.copied": "Copied!",
"comment.copy": "Copy",
"comment.delete": "Delete",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Do you want to unverify {userName}?",
"comment.verified-user": "Verified user",
"comment.verify-user": "Do you want to verify {userName}?",
"comment.vote-error": "Voting error: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Image uploading is disabled for anonymous users. Please log in not as anonymous user to be able to attach images.",
"commentForm.exceeded-size": "{fileName} exceeds size limit of {maxImageSize}",
"commentForm.input-placeholder": "Your comment here",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Anonymous users can't vote",
"vote.deleted": "Can't vote for deleted comment",
"vote.guest": "Sign in to vote",
"vote.only-positive": "Only positive score allowed",
"vote.only-post-page": "Voting allowed only on post's page",
"vote.own-comment": "Can't vote for your own comment",
"vote.readonly": "Can't vote on read-only topics"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Bloqueado",
"comment.blocking-period": "Período de bloqueo",
"comment.cancel": "Cancelar",
"comment.controversy": "Controversia: {value}",
"comment.copied": "¡Copiado!",
"comment.copy": "Copiar",
"comment.delete": "Eliminar",
@@ -61,7 +60,6 @@
"comment.unverify-user": "¿Quieres quitar la verificación a {userName}?",
"comment.verified-user": "Usuario verificado",
"comment.verify-user": "¿Quieres verificar a {userName}?",
"comment.vote-error": "Error al votar: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "La carga de imágenes está deshabilitada para usuarios anónimos. Inicie sesión como usuario no anónimo para poder adjuntar imágenes.",
"commentForm.exceeded-size": "{fileName} excede el tamaño máximo de {maxImageSize}",
"commentForm.input-placeholder": "Tu comentario aquí",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Los usuarios anónimos no puede votar",
"vote.deleted": "No se puede votar un comentario eliminado",
"vote.guest": "Accede para votar",
"vote.only-positive": "El puntaje debe ser positivo",
"vote.only-post-page": "Solo se puede votar en la página de la publicación",
"vote.own-comment": "No puedes votar tu propio comentario",
"vote.readonly": "No se puede votar en tópicos de solo lectura"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Estetty",
"comment.blocking-period": "Eston pituus",
"comment.cancel": "Peruuta",
"comment.controversy": "Kiistely: {value}",
"comment.copied": "Kopioitu!",
"comment.copy": "Kopioi",
"comment.delete": "Poista",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Do you want to unverify {userName}?",
"comment.verified-user": "Vahvistettu käyttäjä",
"comment.verify-user": "Haluatko vahvistaa käyttäjän {userName}?",
"comment.vote-error": "Äänestysvirhe: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Image uploading is disabled for anonymous users. Please log in not as anonymous user to be able to attach images.",
"commentForm.exceeded-size": "{fileName} ylittää {maxImageSize}",
"commentForm.input-placeholder": "Kirjoita kommenttisi tähän",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Anonyymit käyttäjät eivät voi äänestää",
"vote.deleted": "Poistettua kommenttia ei voi äänestää",
"vote.guest": "Kirjaudu sisään äänestääksesi",
"vote.only-positive": "Vain positiivinen arvostelu on sallittu",
"vote.only-post-page": "Äänestys on sallittu vain artikkelisivulla",
"vote.own-comment": "Et voi äänestää oman kommenttisi puolesta",
"vote.readonly": "Et voi äänestää vain luku -tilassa olevien kommenttien puolesta"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Bloqué",
"comment.blocking-period": "Durée de blocage",
"comment.cancel": "Annuler",
"comment.controversy": "Controverse : {value}",
"comment.copied": "Copié !",
"comment.copy": "Copier",
"comment.delete": "Supprimer",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Voulez-vous signaler {userName} comme non vérifié ?",
"comment.verified-user": "Utilisateur vérifié",
"comment.verify-user": "Voulez-vous marquer {userName} comme vérifié ?",
"comment.vote-error": "Erreur de vote : {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Le chargement d'image est désactivé pour les utilisateurs anonymes. Pour joindre des images, veuillez vous connecter.",
"commentForm.exceeded-size": "Le fichier {fileName} dépasse la taille limite de {maxImageSize}",
"commentForm.input-placeholder": "Votre commentaire ici",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Les utilisateurs anonymes ne peuvent pas voter",
"vote.deleted": "Vous ne pouvez pas voter pour un commentaire supprimé",
"vote.guest": "Connectez-vous pour voter",
"vote.only-positive": "Seul un score positif est autorisé",
"vote.only-post-page": "Le vote est possible seulement sur la page de l'article",
"vote.own-comment": "Vous ne pouvez pas voter pour votre propre commentaire.",
"vote.readonly": "Vous ne pouvez pas voter sur un sujet en lecture seule"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Bloccato",
"comment.blocking-period": "Periodo di blocco",
"comment.cancel": "Cancella",
"comment.controversy": "Controverso: {value}",
"comment.copied": "Copiato!",
"comment.copy": "Copia",
"comment.delete": "Elimina",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Vuoi togliere la verifica a {userName}?",
"comment.verified-user": "Utente verificato",
"comment.verify-user": "Vuoi verificare {userName}?",
"comment.vote-error": "Errore nel voto: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Il caricamento delle immagini è disabilitato per gli utenti anonimi. Perfavore accedi come utente non anonimo per poter allegare delle immagini",
"commentForm.exceeded-size": "{fileName} ha superato il limite massimo di {maxImageSize}",
"commentForm.input-placeholder": "Qui il tuo commento",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Gli utenti anonimi non possono votare",
"vote.deleted": "Non puoi votare per un commento eliminato",
"vote.guest": "Accedi per votare",
"vote.only-positive": "Solo punteggi positivi consentiti",
"vote.only-post-page": "Voto consentito solo sulla pagina del post",
"vote.own-comment": "Non puoi votare per il tuo commento",
"vote.readonly": "Non puoi votare sugli argomenti in sola lettura"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "ブロックされました",
"comment.blocking-period": "ブロック期間",
"comment.cancel": "取り消す",
"comment.controversy": "論争: {value}",
"comment.copied": "コピーされました!",
"comment.copy": "コピー",
"comment.delete": "削除",
@@ -61,7 +60,6 @@
"comment.unverify-user": "{userName}を未検証にしますか?",
"comment.verified-user": "検証済みユーザー",
"comment.verify-user": "{userName}を検証済みにしますか?",
"comment.vote-error": "投票エラー: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "匿名ユーザーは画像をアップロードできません。画像を添付するには、匿名ではないユーザーでログインしてください。",
"commentForm.exceeded-size": "{fileName} のサイズが上限値の {maxImageSize} を超えています",
"commentForm.input-placeholder": "コメントをここに入力してください",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "匿名ユーザーは投稿できません",
"vote.deleted": "削除済みコメントには投票できません",
"vote.guest": "投票するにはサインインしてください",
"vote.only-positive": "正のスコアのみが許可されています",
"vote.only-post-page": "記事ページでのみ投票できます",
"vote.own-comment": "自分のコメントには投票できません",
"vote.readonly": "読み取り専用のトピックには投票できません"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "차단됨",
"comment.blocking-period": "차단 기간",
"comment.cancel": "취소",
"comment.controversy": "논쟁: {value}",
"comment.copied": "복사됨!",
"comment.copy": "복사",
"comment.delete": "삭제",
@@ -61,7 +60,6 @@
"comment.unverify-user": "{userName} 님을 미확인 처리하시겠어요?",
"comment.verified-user": "확인된 사용자",
"comment.verify-user": "{userName} 님을 확인 처리하시겠어요?",
"comment.vote-error": "투표 오류: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "익명의 사용자는 이미지를 업로드할 수 없습니다. 이미지를 첨부하려면 익명이 아닌 사용자로 로그인하세요.",
"commentForm.exceeded-size": "{fileName}의 크기가 {maxImageSize} 한도를 초과합니다",
"commentForm.input-placeholder": "코멘트를 입력하세요",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "익명의 사용자는 투표할 수 없습니다",
"vote.deleted": "삭제된 댓글에는 투표할 수 없습니다",
"vote.guest": "투표하려면 로그인하세요",
"vote.only-positive": "점수는 양수로만 입력할 수 있습니다",
"vote.only-post-page": "게시물의 페이지에서만 투표할 수 있습니다",
"vote.own-comment": "자신의 댓글에는 투표할 수 없습니다",
"vote.readonly": "읽기 전용 항목에는 투표할 수 없습니다"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Zablokowany",
"comment.blocking-period": "Okres zablokowania",
"comment.cancel": "Anuluj",
"comment.controversy": "Spór: {value}",
"comment.copied": "Skopiowano!",
"comment.copy": "Skopiuj",
"comment.delete": "Usuń",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Czy chcesz cofnąć weryfikację użytkownika {userName}?",
"comment.verified-user": "Zweryfikowany użytkownik",
"comment.verify-user": "Czy chcesz zweryfikować użytkownika {userName}?",
"comment.vote-error": "Błąd głosowania: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Dodawanie zdjęć jest wyłączone dla anonimowych użytkowników. Zaloguj się by mieć możliwość dodawania zdjęć.",
"commentForm.exceeded-size": "{fileName} przekracza maksymalny limit {maxImageSize}",
"commentForm.input-placeholder": "Twój komentarz tutaj",
@@ -167,11 +165,8 @@
"user.comments": "Komentarze",
"user.load-more": "Load more",
"user.my-comments": "Moje komentarze",
"vote.anonymous": "Anonimowi użytkownicy nie mogą głosować",
"vote.deleted": "Nie można głosować na usunięte komentarze",
"vote.guest": "Zaloguj się żeby głosować",
"vote.only-positive": "Dozwolony tylko wynik pozytywny",
"vote.only-post-page": "Głosowanie możliwe tylko na stronie posta",
"vote.own-comment": "Nie można głosować na swoje własne komentarze",
"vote.readonly": "Nie można głosować na wątki tylko do odczytu"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Заблокирован",
"comment.blocking-period": "Период блокировки",
"comment.cancel": "Отменить",
"comment.controversy": "Спорность: {value}",
"comment.copied": "Скопировано!",
"comment.copy": "Копировать",
"comment.delete": "Удалить",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Отменить подтверждение учетной записи для {userName}?",
"comment.verified-user": "Подтвержденная учетная запись",
"comment.verify-user": "Подтвердить учетную запись для {userName}?",
"comment.vote-error": "Ошибка голосования: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Анонимные пользователи не могут загружать изображения. Для загрузки изображений войдите под своим именем.",
"commentForm.exceeded-size": "Размер файла {fileName} не должен превышать {maxImageSize}",
"commentForm.input-placeholder": "Оставьте здесь свой комментарий",
@@ -167,11 +165,8 @@
"user.comments": "Комментарии",
"user.load-more": "Загрузить ещё",
"user.my-comments": "Мои комментарии",
"vote.anonymous": "Анонимные пользователи не могут голосовать",
"vote.deleted": "Нельзя голосовать за удаленный комментарий",
"vote.guest": "Войдите, чтобы проголосовать",
"vote.only-positive": "Разрешены только положительные оценки",
"vote.only-post-page": "Голосовать можно только на странице поста",
"vote.own-comment": "Вы не можете голосовать за свои комментарии",
"vote.readonly": "Вы не можете голосовать за комментарии, которые доступны только для чтения"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Engellendi",
"comment.blocking-period": "Engelleme süresi",
"comment.cancel": "İptal",
"comment.controversy": "Tartışma: {value}",
"comment.copied": "Kopyalandı!",
"comment.copy": "Kopyala",
"comment.delete": "Sil",
@@ -61,7 +60,6 @@
"comment.unverify-user": "{userName} kullanıcısının onayı kaldırılsın mı?",
"comment.verified-user": "Onaylanmış kullanıcı",
"comment.verify-user": "{userName} kullanıcısı onaylansın mı?",
"comment.vote-error": "Oylama hatası: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Anonim kullanıcılar resim yükleme özelliğini kullanamazlar, lütfen resim ekleyebilmek için giriş yapın.",
"commentForm.exceeded-size": "{fileName} dosyası boyut limitini ({maxImageSize}) aşıyor",
"commentForm.input-placeholder": "Bir yorum yazın",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Anonim kullanıcılar oy kullanamaz",
"vote.deleted": "Silinmiş yorum oylanamaz",
"vote.guest": "Oy vermek için giriş yapın",
"vote.only-positive": "Sadece pozitif oy verilebilir",
"vote.only-post-page": "Yalnızca gönderinin sayfasında oylama yapılabilir",
"vote.own-comment": "Kendi yorumunuza oy veremezsiniz",
"vote.readonly": "Yazmaya kapalı başlıklarda oy veremezsiniz"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Заблокованний",
"comment.blocking-period": "Період блокування",
"comment.cancel": "Відмінити",
"comment.controversy": "Суперечливість: {value}",
"comment.copied": "Скопійовано!",
"comment.copy": "Скопіювати",
"comment.delete": "Видалити",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Прибрати статус справжності облікового запису для{userName}?",
"comment.verified-user": "Справжність облікового запису",
"comment.verify-user": "Підтвердити справжність облікового запису для{userName}?",
"comment.vote-error": "Помилка голосування: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Завантаження зображень анонімними користувачами заборонено. Будь ласка, увійдіть як не анонімний користувач.",
"commentForm.exceeded-size": "Розмір файлу {fileName} повинен бути менший ніж {maxImageSize}",
"commentForm.input-placeholder": "Написати коментар",
@@ -167,11 +165,8 @@
"user.comments": "Коментарі",
"user.load-more": "Load more",
"user.my-comments": "Мої коментарі",
"vote.anonymous": "Не можна голосувати анонімному користувачу",
"vote.deleted": "Не можна голосувати за видалений коментар",
"vote.guest": "Увійдіть в систему для голосування",
"vote.only-positive": "Дозволені тільки позитивні оцінки",
"vote.only-post-page": "Голосування можливо тільки для статей",
"vote.own-comment": "Ви не можете голосувати за свої коментарі",
"vote.readonly": "Не можна голосувати за коментарі, які в режимі тільки для читання"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "Đã chặn",
"comment.blocking-period": "Thời gian chặn",
"comment.cancel": "Huỷ",
"comment.controversy": "Tranh luận: {value}",
"comment.copied": "Đã sao chép!",
"comment.copy": "Sao chép",
"comment.delete": "Xoá",
@@ -61,7 +60,6 @@
"comment.unverify-user": "Bạn có muốn bỏ xác thực {userName}?",
"comment.verified-user": "Người dùng đã xác thực",
"comment.verify-user": "Bạn có muốn xác thực {userName}?",
"comment.vote-error": "Vote bị lỗi: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "Upload ảnh bị tắt đối với người dùng ẩn danh. Vui lòng đăng nhập không phải ẩn danh để có thể đính kèm hình ảnh.",
"commentForm.exceeded-size": "{fileName} vượt kích thước giới hạn {maxImageSize}",
"commentForm.input-placeholder": "Nhập bình luận của bạn tại đây",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "Người dùng ẩn danh không thể vote",
"vote.deleted": "Không thể vote bình luận đã xoá",
"vote.guest": "Đăng nhập để vote",
"vote.only-positive": "Chỉ cho phép điểm số dương",
"vote.only-post-page": "Chỉ được phép vote trên trang của bài đăng",
"vote.own-comment": "Bạn không thể vote bình luận của bạn",
"vote.readonly": "Không thể Vote chủ đề chỉ đọc"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+4 -9
View File
@@ -36,7 +36,6 @@
"comment.blocked-user": "已封锁用户",
"comment.blocking-period": "封锁期限",
"comment.cancel": "取消",
"comment.controversy": "争议:{value}",
"comment.copied": "已复制!",
"comment.copy": "复制",
"comment.delete": "删除",
@@ -61,7 +60,6 @@
"comment.unverify-user": "您是否要为 {userName} 取消验证?",
"comment.verified-user": "已验证的用户",
"comment.verify-user": "您是否要验证 {userName}?",
"comment.vote-error": "投票失败: {voteErrorMessage}",
"commentForm.anonymous-uploading-disabled": "匿名用户无法上传图片。上传之前,您应该先以非匿名用户登录。",
"commentForm.exceeded-size": "{fileName} 超出了 {maxImageSize} 大小的限制",
"commentForm.input-placeholder": "在此填写评论",
@@ -167,11 +165,8 @@
"user.comments": "Comments",
"user.load-more": "Load more",
"user.my-comments": "My comments",
"vote.anonymous": "匿名用户无法投票",
"vote.deleted": "无法为已删除的评论投票",
"vote.guest": "登录以投票",
"vote.only-positive": "只允许正面分数",
"vote.only-post-page": "只能在文章页面上投票",
"vote.own-comment": "无法为自己的评论投票",
"vote.readonly": "无法为只读主题投票"
"vote.controversy": "Controversy: {value}",
"vote.downvote": "Vote down",
"vote.score": "Votes score",
"vote.upvote": "Vote up"
}
+14 -12
View File
@@ -13,6 +13,9 @@ import {
COMMENTS_SET_SORT,
COMMENTS_REQUEST_FETCHING,
COMMENTS_REQUEST_SUCCESS,
COMMENT_PATCH,
COMMENT_PATCH_ACTION,
COMMENTS_EDIT_ACTION,
} from './types';
import { setItem } from 'common/local-storage';
import { LS_SORT_KEY } from 'common/constants';
@@ -35,21 +38,20 @@ export const addComment =
dispatch({ type: COMMENTS_APPEND, pid: pid || null, comment });
};
function editComments(comment: Comment): COMMENTS_EDIT_ACTION {
return { type: COMMENTS_EDIT, comment };
}
export function patchComment(patch: Pick<Comment, 'id'> & Partial<Comment>): COMMENT_PATCH_ACTION {
return { type: COMMENT_PATCH, patch };
}
/** edits comment in tree */
export const updateComment =
(id: Comment['id'], text: string): StoreAction<Promise<void>> =>
async (dispatch) => {
const comment = await api.updateComment({ id, text });
dispatch({ type: COMMENTS_EDIT, comment });
};
/** edits comment in tree */
export const putVote =
(id: Comment['id'], value: number): StoreAction<Promise<void>> =>
async (dispatch) => {
await api.putCommentVote({ id, value });
const comment = await api.getComment(id);
dispatch({ type: COMMENTS_EDIT, comment });
dispatch(editComments(comment));
};
/** edits comment in tree */
@@ -63,7 +65,7 @@ export const setPinState =
}
let comment = getState().comments.allComments[id];
comment = { ...comment, pin: value, edit: { summary: '', time: new Date().toISOString() } };
dispatch({ type: COMMENTS_EDIT, comment });
dispatch(editComments(comment));
};
/** edits comment in tree */
@@ -79,7 +81,7 @@ export const removeComment =
}
let comment = getState().comments.allComments[id];
comment = { ...comment, delete: true, edit: { summary: '', time: new Date().toISOString() } };
dispatch({ type: COMMENTS_EDIT, comment });
dispatch(editComments(comment));
};
/** fetches comments from server */
+12 -2
View File
@@ -18,6 +18,8 @@ import {
COMMENTS_REQUEST_SUCCESS,
COMMENTS_REQUEST_FAILURE,
COMMENTS_REQUEST_ACTIONS,
COMMENT_PATCH,
COMMENT_PATCH_ACTION,
} from './types';
import { getPinnedComments, getInitialSort } from './utils';
import { cmpRef } from 'utils/cmpRef';
@@ -96,7 +98,12 @@ const reduceComments = (c: Record<Comment['id'], Comment>, x: Node): Record<Comm
export const allComments = (
state: Record<Comment['id'], Comment> = {},
action: COMMENTS_SET_ACTION | COMMENTS_APPEND_ACTION | COMMENTS_EDIT_ACTION | COMMENTS_PATCH_ACTION
action:
| COMMENTS_SET_ACTION
| COMMENTS_APPEND_ACTION
| COMMENTS_EDIT_ACTION
| COMMENTS_PATCH_ACTION
| COMMENT_PATCH_ACTION
): Record<Comment['id'], Comment> => {
switch (action.type) {
case COMMENTS_SET: {
@@ -106,6 +113,9 @@ export const allComments = (
case COMMENTS_EDIT: {
return { ...state, [action.comment.id]: action.comment };
}
case COMMENT_PATCH: {
return { ...state, [action.patch.id]: { ...state[action.patch.id], ...action.patch } };
}
case COMMENTS_PATCH: {
let newState = state;
let changed = false;
@@ -142,7 +152,7 @@ export const activeComment = (
export const pinnedComments = (
state: Comment['id'][] = [],
action: COMMENTS_SET_ACTION | COMMENTS_EDIT_ACTION | COMMENTS_PATCH_ACTION
action: COMMENTS_SET_ACTION | COMMENTS_EDIT_ACTION | COMMENTS_PATCH_ACTION | COMMENT_PATCH_ACTION
): Comment['id'][] => {
switch (action.type) {
case COMMENTS_SET: {
+8
View File
@@ -1,6 +1,13 @@
import { Node, Comment, Sorting } from 'common/types';
import { StoreState } from '../index';
export const COMMENT_PATCH = 'COMMENT/PATCH';
export interface COMMENT_PATCH_ACTION {
type: typeof COMMENT_PATCH;
patch: Pick<Comment, 'id'> & Partial<Comment>;
}
export const COMMENTS_SET = 'COMMENTS/SET';
export interface COMMENTS_SET_ACTION {
@@ -58,6 +65,7 @@ export interface COMMENTS_SET_SORT_ACTION {
}
export type COMMENTS_ACTIONS =
| COMMENT_PATCH_ACTION
| COMMENTS_SET_ACTION
| COMMENTS_APPEND_ACTION
| COMMENTS_EDIT_ACTION
+7 -2
View File
@@ -38,13 +38,13 @@
--color42: #c6efef;
--color48: rgba(37, 156, 154, 0.6);
--color47: rgba(37, 156, 154, 0.4);
--color12: #259e06;
--color28: #672323;
--color25: #9a0000;
--color30: #cc0606;
--color38: #ef0000;
--color27: #f98989;
--color26: #ffd7d7;
--color30: 204, 6, 6;
--color12: 37, 158, 6;
/* code-highlight */
--chroma-bg: rgba(0, 0, 0, 0.05);
@@ -61,8 +61,10 @@
/* Named variables */
--primary-color: 0, 170, 170;
--primary-brighter-color: 0, 153, 153;
--primary-darker-color: 0, 102, 102;
--primary-text-color: 38, 38, 38;
--secondary-text-color: 100, 116, 139;
--secondary-darker-text-color: 150, 150, 150;
--primary-background-color: 255, 255, 255;
--black-color: 0, 0, 0;
--white-color: 255, 255, 255;
@@ -70,6 +72,8 @@
--error-background: #ff466f2b;
--line-color: var(--color16);
--line-brighter-color: var(--color31);
--text-color: var(--black-color);
--transparent: transparent;
}
:root .dark {
@@ -81,6 +85,7 @@
--secondary-text-color: 209, 213, 219;
--error-color: #ffa0a0;
--line-brighter-color: var(--color11);
--text-color: var(--white-color);
color-scheme: dark;
}