diff --git a/frontend/app/common/api.ts b/frontend/app/common/api.ts index 5722df53..717e8a69 100644 --- a/frontend/app/common/api.ts +++ b/frontend/app/common/api.ts @@ -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 => - apiFetcher.put(`/vote/${id}`, { url, vote: value }); +export const putCommentVote = ({ + id, + vote, +}: { + id: Comment['id']; + vote: number; +}): Promise> => apiFetcher.put(`/vote/${id}`, { url, vote }); export const addComment = ({ title, diff --git a/frontend/app/components/comment/__score-value/comment__score-value.css b/frontend/app/components/comment/__score-value/comment__score-value.css deleted file mode 100644 index cb61f7fe..00000000 --- a/frontend/app/components/comment/__score-value/comment__score-value.css +++ /dev/null @@ -1,7 +0,0 @@ -.comment__score-value { - display: inline-block; - min-width: 24px; - vertical-align: middle; - text-align: center; - font-size: 14px; -} diff --git a/frontend/app/components/comment/__score/comment__score.css b/frontend/app/components/comment/__score/comment__score.css deleted file mode 100644 index 7c4c9f7e..00000000 --- a/frontend/app/components/comment/__score/comment__score.css +++ /dev/null @@ -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; -} diff --git a/frontend/app/components/comment/__vote/_disabled/comment__vote_disabled.css b/frontend/app/components/comment/__vote/_disabled/comment__vote_disabled.css deleted file mode 100644 index 16147d19..00000000 --- a/frontend/app/components/comment/__vote/_disabled/comment__vote_disabled.css +++ /dev/null @@ -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; - } - } -} diff --git a/frontend/app/components/comment/__vote/_disabled/comment__vote_disabled.svg b/frontend/app/components/comment/__vote/_disabled/comment__vote_disabled.svg deleted file mode 100644 index 0d1070c5..00000000 --- a/frontend/app/components/comment/__vote/_disabled/comment__vote_disabled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/app/components/comment/__vote/_selected/comment__vote_selected.css b/frontend/app/components/comment/__vote/_selected/comment__vote_selected.css deleted file mode 100644 index 28016146..00000000 --- a/frontend/app/components/comment/__vote/_selected/comment__vote_selected.css +++ /dev/null @@ -1,3 +0,0 @@ -.comment__vote_selected { - cursor: default; -} diff --git a/frontend/app/components/comment/__vote/_type/_down/comment__vote_type_down.css b/frontend/app/components/comment/__vote/_type/_down/comment__vote_type_down.css deleted file mode 100644 index e9e91d3e..00000000 --- a/frontend/app/components/comment/__vote/_type/_down/comment__vote_type_down.css +++ /dev/null @@ -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'); - } -} diff --git a/frontend/app/components/comment/__vote/_type/_down/comment__vote_type_down.svg b/frontend/app/components/comment/__vote/_type/_down/comment__vote_type_down.svg deleted file mode 100644 index d29d4a65..00000000 --- a/frontend/app/components/comment/__vote/_type/_down/comment__vote_type_down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/app/components/comment/__vote/_type/_up/comment__vote_type_up.css b/frontend/app/components/comment/__vote/_type/_up/comment__vote_type_up.css deleted file mode 100644 index f71000f9..00000000 --- a/frontend/app/components/comment/__vote/_type/_up/comment__vote_type_up.css +++ /dev/null @@ -1,8 +0,0 @@ -.comment__vote_type_up { - margin-right: 4px; - - &.comment__vote_selected, - &:hover { - background-image: url('./comment__vote_type_up.svg'); - } -} diff --git a/frontend/app/components/comment/__vote/_type/_up/comment__vote_type_up.svg b/frontend/app/components/comment/__vote/_type/_up/comment__vote_type_up.svg deleted file mode 100644 index 463ab2cd..00000000 --- a/frontend/app/components/comment/__vote/_type/_up/comment__vote_type_up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/app/components/comment/__vote/comment__vote.css b/frontend/app/components/comment/__vote/comment__vote.css deleted file mode 100644 index e6eecdd6..00000000 --- a/frontend/app/components/comment/__vote/comment__vote.css +++ /dev/null @@ -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; -} diff --git a/frontend/app/components/comment/__vote/comment__vote.svg b/frontend/app/components/comment/__vote/comment__vote.svg deleted file mode 100644 index 69dce428..00000000 --- a/frontend/app/components/comment/__vote/comment__vote.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/app/components/comment/_collapsed/comment_collapsed.css b/frontend/app/components/comment/_collapsed/comment_collapsed.css index c21a890e..8d051149 100644 --- a/frontend/app/components/comment/_collapsed/comment_collapsed.css +++ b/frontend/app/components/comment/_collapsed/comment_collapsed.css @@ -24,8 +24,4 @@ & .comment__time { font-style: italic; } - - & .comment__score { - top: 3px; - } } diff --git a/frontend/app/components/comment/_editing/comment_editing.css b/frontend/app/components/comment/_editing/comment_editing.css index 9eb94d5c..a3343792 100644 --- a/frontend/app/components/comment/_editing/comment_editing.css +++ b/frontend/app/components/comment/_editing/comment_editing.css @@ -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; - } } } diff --git a/frontend/app/components/comment/_replying/comment_replying.css b/frontend/app/components/comment/_replying/comment_replying.css index 41070ac1..41dac2d4 100644 --- a/frontend/app/components/comment/_replying/comment_replying.css +++ b/frontend/app/components/comment/_replying/comment_replying.css @@ -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; - } } } diff --git a/frontend/app/components/comment/_theme/_dark/comment_theme_dark.css b/frontend/app/components/comment/_theme/_dark/comment_theme_dark.css index 53182594..3f4ab19f 100644 --- a/frontend/app/components/comment/_theme/_dark/comment_theme_dark.css +++ b/frontend/app/components/comment/_theme/_dark/comment_theme_dark.css @@ -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); } diff --git a/frontend/app/components/comment/_theme/_light/comment_theme_light.css b/frontend/app/components/comment/_theme/_light/comment_theme_light.css index 3bfd68ef..ad8157ca 100644 --- a/frontend/app/components/comment/_theme/_light/comment_theme_light.css +++ b/frontend/app/components/comment/_theme/_light/comment_theme_light.css @@ -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); } diff --git a/frontend/app/components/comment/comment-votes.module.css b/frontend/app/components/comment/comment-votes.module.css new file mode 100644 index 00000000..ea69aacb --- /dev/null +++ b/frontend/app/components/comment/comment-votes.module.css @@ -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; +} diff --git a/frontend/app/components/comment/comment-votes.spec.tsx b/frontend/app/components/comment/comment-votes.spec.tsx new file mode 100644 index 00000000..2f02109f --- /dev/null +++ b/frontend/app/components/comment/comment-votes.spec.tsx @@ -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('', () => { + it('should render vote component', () => { + render(); + 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(); + expect(screen.getByTitle('Votes score')).toBeVisible(); + }); + it('should render vote component with negative score', () => { + render(); + 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(); + 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(); + expect(screen.getByTitle('Vote up')).toBeDisabled(); + }); + + it('should disable downvote button when downvoted', () => { + render(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + expect(screen.getByTitle('Vote down')).not.toBeDisabled(); + fireEvent(screen.getByTitle('Vote down'), new Event('click')); + await waitFor(() => { + expect(screen.getByTitle('Vote down')).toBeDisabled(); + }); + }); +}); diff --git a/frontend/app/components/comment/comment-votes.tsx b/frontend/app/components/comment/comment-votes.tsx new file mode 100644 index 00000000..771b3142 --- /dev/null +++ b/frontend/app/components/comment/comment-votes.tsx @@ -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(undefined); + + async function handleClick(evt: preact.JSX.TargetedEvent) { + 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 ( + + {Boolean(!disabled && !positiveScore) && ( + + )} + {errorMessage} : undefined} + position="top-left" + hideBehavior="mouseleave" + hideTimeout={10000} + permanent + onHide={() => { + setErrorMessage(undefined); + }} + > +
0, + })} + > + {loadingState?.votes ?? votes} +
+
+ {!disabled && ( + + )} +
+ ); +} + +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}', + }, +}); diff --git a/frontend/app/components/comment/comment.test.tsx b/frontend/app/components/comment/comment.test.tsx index 2605bc84..9b20d86b 100644 --- a/frontend/app/components/comment/comment.test.tsx +++ b/frontend/app/components/comment/comment.test.tsx @@ -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 ; @@ -21,43 +21,67 @@ function CommentWithIntl(props: CommentProps) { function mountComment(props: CommentProps) { return mount( - + + + ); } -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('', () => { + 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(); @@ -68,28 +92,25 @@ describe('', () => { describe('verification', () => { it('should render active verification icon', () => { - const props = getDefaultProps(); props.data.user.verified = true; render(); expect(screen.getByTitle('Verified user')).toBeVisible(); }); it('should not render verification icon', () => { - const props = getDefaultProps(); + const props = getProps(); render(); 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(); 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(); expect(screen.queryByTitle('Verified user')).toBeVisible(); @@ -97,154 +118,77 @@ describe('', () => { }); 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(); + 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(); + 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('', () => { }); 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('', () => { 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('', () => { 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); }); }); diff --git a/frontend/app/components/comment/comment.tsx b/frontend/app/components/comment/comment.tsx index e67c181d..a3cc603a 100644 --- a/frontend/app/components/comment/comment.tsx +++ b/frontend/app/components/comment/comment.tsx @@ -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 { /** comment text node. Used in comment text copying */ textNode = createRef(); - updateState(props: CommentProps) { - const newState: Partial = { - scoreDelta: props.data.vote, - cachedScore: props.data.score, - }; + updateState = (props: CommentProps) => { + const newState: Partial = {}; if (props.inView) { newState.renderDummy = false; @@ -106,15 +89,13 @@ export class Comment extends Component { } 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 { 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 { 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 { 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 { 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 { 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 { 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 { )} - - - - Vote up - - - - {o.score.sign} - {o.score.value} - - - - Vote down - - + {this.props.view !== 'pinned' && ( + + )}
- {!!state.voteErrorMessage && ( -
- -
- )} - {(!props.collapsed || props.view === 'pinned') && (
', () => { + it('should be rendered with default size', async () => { + render(); + 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(); + expect(await screen.findByTitle('icon')).toHaveAttribute('width', '16'); + expect(await screen.findByTitle('icon')).toHaveAttribute('height', '16'); + }); +}); diff --git a/frontend/app/components/icons/arrow.tsx b/frontend/app/components/icons/arrow.tsx index 08dce458..63f20e3c 100644 --- a/frontend/app/components/icons/arrow.tsx +++ b/frontend/app/components/icons/arrow.tsx @@ -1,6 +1,6 @@ import { h, JSX } from 'preact'; -type Props = { size?: number } & JSX.HTMLAttributes; +type Props = JSX.HTMLAttributes & { size?: number }; export function ArrowIcon({ size = 14, ...props }: Props) { return ( diff --git a/frontend/app/components/tooltip/index.ts b/frontend/app/components/tooltip/index.ts new file mode 100644 index 00000000..ed8326d5 --- /dev/null +++ b/frontend/app/components/tooltip/index.ts @@ -0,0 +1 @@ +export * from './tooltip'; diff --git a/frontend/app/components/tooltip/tooltip.module.css b/frontend/app/components/tooltip/tooltip.module.css new file mode 100644 index 00000000..916d437f --- /dev/null +++ b/frontend/app/components/tooltip/tooltip.module.css @@ -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; +} diff --git a/frontend/app/components/tooltip/tooltip.spec.tsx b/frontend/app/components/tooltip/tooltip.spec.tsx new file mode 100644 index 00000000..0f0cee91 --- /dev/null +++ b/frontend/app/components/tooltip/tooltip.spec.tsx @@ -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('', () => { + it('should not render tooltip without content', () => { + render(Hello); + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + }); + + it('should not render tooltip with content', () => { + render( + + Hello + + ); + 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( + + Hello + + ); + expect(screen.queryByRole('tooltip')).toHaveClass(position); + }); +}); diff --git a/frontend/app/components/tooltip/tooltip.tsx b/frontend/app/components/tooltip/tooltip.tsx new file mode 100644 index 00000000..12250f54 --- /dev/null +++ b/frontend/app/components/tooltip/tooltip.tsx @@ -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 ( +
+ {content && ( +
+ {content} +
+ )} + {children} +
+ ); +} diff --git a/frontend/app/locales/ar.json b/frontend/app/locales/ar.json index e2da53a9..c9957ff6 100644 --- a/frontend/app/locales/ar.json +++ b/frontend/app/locales/ar.json @@ -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": "تنميط الكتابة بالماركداون مدعوم", - "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": "صنع باستخدام Remark42", - "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": "لا يمكنك التصويت على مواضيع للقراءة فقط." - } \ No newline at end of file +{ + "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": "تنميط الكتابة بالماركداون مدعوم", + "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": "صنع باستخدام Remark42", + "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" +} diff --git a/frontend/app/locales/be.json b/frontend/app/locales/be.json index d27e6e6d..c7b19a94 100644 --- a/frontend/app/locales/be.json +++ b/frontend/app/locales/be.json @@ -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" } diff --git a/frontend/app/locales/bg.json b/frontend/app/locales/bg.json index 8ceeee27..dd87fb02 100644 --- a/frontend/app/locales/bg.json +++ b/frontend/app/locales/bg.json @@ -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" } diff --git a/frontend/app/locales/bp.json b/frontend/app/locales/bp.json index 614a0ecc..7ca23a61 100644 --- a/frontend/app/locales/bp.json +++ b/frontend/app/locales/bp.json @@ -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" } diff --git a/frontend/app/locales/de.json b/frontend/app/locales/de.json index 833bc4c9..dcb430d1 100644 --- a/frontend/app/locales/de.json +++ b/frontend/app/locales/de.json @@ -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" } diff --git a/frontend/app/locales/en.json b/frontend/app/locales/en.json index 8b2f6c35..e2f82865 100644 --- a/frontend/app/locales/en.json +++ b/frontend/app/locales/en.json @@ -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" } diff --git a/frontend/app/locales/es.json b/frontend/app/locales/es.json index 3c2b2508..571d2e2e 100644 --- a/frontend/app/locales/es.json +++ b/frontend/app/locales/es.json @@ -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" } diff --git a/frontend/app/locales/fi.json b/frontend/app/locales/fi.json index d6017cf9..607c742c 100644 --- a/frontend/app/locales/fi.json +++ b/frontend/app/locales/fi.json @@ -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" } diff --git a/frontend/app/locales/fr.json b/frontend/app/locales/fr.json index 97973fc9..8d70b027 100644 --- a/frontend/app/locales/fr.json +++ b/frontend/app/locales/fr.json @@ -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" } diff --git a/frontend/app/locales/it.json b/frontend/app/locales/it.json index 071ad6f1..ff10b4f6 100644 --- a/frontend/app/locales/it.json +++ b/frontend/app/locales/it.json @@ -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" } diff --git a/frontend/app/locales/ja.json b/frontend/app/locales/ja.json index b4ee4398..8e2a4fb5 100644 --- a/frontend/app/locales/ja.json +++ b/frontend/app/locales/ja.json @@ -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" } diff --git a/frontend/app/locales/ko.json b/frontend/app/locales/ko.json index acfe8fa4..c0045fa5 100644 --- a/frontend/app/locales/ko.json +++ b/frontend/app/locales/ko.json @@ -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" } diff --git a/frontend/app/locales/pl.json b/frontend/app/locales/pl.json index 502c6feb..f2c1a7a8 100644 --- a/frontend/app/locales/pl.json +++ b/frontend/app/locales/pl.json @@ -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" } diff --git a/frontend/app/locales/ru.json b/frontend/app/locales/ru.json index 19bf89ab..87a5da24 100644 --- a/frontend/app/locales/ru.json +++ b/frontend/app/locales/ru.json @@ -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" } diff --git a/frontend/app/locales/tr.json b/frontend/app/locales/tr.json index 714e9198..c1eb3c44 100644 --- a/frontend/app/locales/tr.json +++ b/frontend/app/locales/tr.json @@ -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" } diff --git a/frontend/app/locales/ua.json b/frontend/app/locales/ua.json index bf5f7088..c7aeaa69 100644 --- a/frontend/app/locales/ua.json +++ b/frontend/app/locales/ua.json @@ -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" } diff --git a/frontend/app/locales/vi.json b/frontend/app/locales/vi.json index f733d796..88c1abd0 100644 --- a/frontend/app/locales/vi.json +++ b/frontend/app/locales/vi.json @@ -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" } diff --git a/frontend/app/locales/zh.json b/frontend/app/locales/zh.json index 843b2191..c1f81f57 100644 --- a/frontend/app/locales/zh.json +++ b/frontend/app/locales/zh.json @@ -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" } diff --git a/frontend/app/store/comments/actions.ts b/frontend/app/store/comments/actions.ts index 099e2cc6..95ee9771 100644 --- a/frontend/app/store/comments/actions.ts +++ b/frontend/app/store/comments/actions.ts @@ -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 & Partial): COMMENT_PATCH_ACTION { + return { type: COMMENT_PATCH, patch }; +} + /** edits comment in tree */ export const updateComment = (id: Comment['id'], text: string): StoreAction> => 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> => - 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 */ diff --git a/frontend/app/store/comments/reducers.ts b/frontend/app/store/comments/reducers.ts index 777d2fd4..cb4c4e7f 100644 --- a/frontend/app/store/comments/reducers.ts +++ b/frontend/app/store/comments/reducers.ts @@ -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, x: Node): Record = {}, - 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 => { 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: { diff --git a/frontend/app/store/comments/types.ts b/frontend/app/store/comments/types.ts index bef652ea..941024df 100644 --- a/frontend/app/store/comments/types.ts +++ b/frontend/app/store/comments/types.ts @@ -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 & Partial; +} + 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 diff --git a/frontend/app/styles/custom-properties.css b/frontend/app/styles/custom-properties.css index 9f6da7a8..aae5d2b3 100644 --- a/frontend/app/styles/custom-properties.css +++ b/frontend/app/styles/custom-properties.css @@ -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; }