diff --git a/frontend/app/components/auth/components/assets/dev.svg b/frontend/app/assets/social/dev.svg similarity index 100% rename from frontend/app/components/auth/components/assets/dev.svg rename to frontend/app/assets/social/dev.svg diff --git a/frontend/app/components/auth/components/assets/facebook.svg b/frontend/app/assets/social/facebook.svg similarity index 100% rename from frontend/app/components/auth/components/assets/facebook.svg rename to frontend/app/assets/social/facebook.svg diff --git a/frontend/app/components/auth/components/assets/github-dark.svg b/frontend/app/assets/social/github-dark.svg similarity index 100% rename from frontend/app/components/auth/components/assets/github-dark.svg rename to frontend/app/assets/social/github-dark.svg diff --git a/frontend/app/components/auth/components/assets/github-light.svg b/frontend/app/assets/social/github-light.svg similarity index 100% rename from frontend/app/components/auth/components/assets/github-light.svg rename to frontend/app/assets/social/github-light.svg diff --git a/frontend/app/components/auth/components/assets/google.svg b/frontend/app/assets/social/google.svg similarity index 100% rename from frontend/app/components/auth/components/assets/google.svg rename to frontend/app/assets/social/google.svg diff --git a/frontend/app/components/auth/components/assets/microsoft.svg b/frontend/app/assets/social/microsoft.svg similarity index 100% rename from frontend/app/components/auth/components/assets/microsoft.svg rename to frontend/app/assets/social/microsoft.svg diff --git a/frontend/app/components/auth/components/assets/patreon.svg b/frontend/app/assets/social/patreon.svg similarity index 100% rename from frontend/app/components/auth/components/assets/patreon.svg rename to frontend/app/assets/social/patreon.svg diff --git a/frontend/app/components/auth/components/assets/telegram.svg b/frontend/app/assets/social/telegram.svg similarity index 100% rename from frontend/app/components/auth/components/assets/telegram.svg rename to frontend/app/assets/social/telegram.svg diff --git a/frontend/app/components/auth/components/assets/twitter.svg b/frontend/app/assets/social/twitter.svg similarity index 100% rename from frontend/app/components/auth/components/assets/twitter.svg rename to frontend/app/assets/social/twitter.svg diff --git a/frontend/app/components/auth/components/assets/yandex.svg b/frontend/app/assets/social/yandex.svg similarity index 100% rename from frontend/app/components/auth/components/assets/yandex.svg rename to frontend/app/assets/social/yandex.svg diff --git a/frontend/app/common/types.ts b/frontend/app/common/types.ts index 46a67d8af3..7e64900d17 100644 --- a/frontend/app/common/types.ts +++ b/frontend/app/common/types.ts @@ -7,6 +7,7 @@ export type User = { block: boolean; verified: boolean; email_subscription?: boolean; + paid_sub?: boolean; }; /** data which is used on user-info page */ diff --git a/frontend/app/components/auth/components/oauth.consts.ts b/frontend/app/components/auth/components/oauth.consts.ts index e10e80b8f1..afa7ab973d 100644 --- a/frontend/app/components/auth/components/oauth.consts.ts +++ b/frontend/app/components/auth/components/oauth.consts.ts @@ -1,19 +1,19 @@ export const OAUTH_DATA = { - facebook: require('./assets/facebook.svg').default as string, - twitter: require('./assets/twitter.svg').default as string, - patreon: require('./assets/patreon.svg').default as string, - google: require('./assets/google.svg').default as string, - microsoft: require('./assets/microsoft.svg').default as string, - yandex: require('./assets/yandex.svg').default as string, - dev: require('./assets/dev.svg').default as string, + facebook: require('assets/social/facebook.svg').default as string, + twitter: require('assets/social/twitter.svg').default as string, + patreon: require('assets/social/patreon.svg').default as string, + google: require('assets/social/google.svg').default as string, + microsoft: require('assets/social/microsoft.svg').default as string, + yandex: require('assets/social/yandex.svg').default as string, + dev: require('assets/social/dev.svg').default as string, github: { name: 'GitHub', icons: { - light: require('./assets/github-light.svg').default as string, - dark: require('./assets/github-dark.svg').default as string, + light: require('assets/social/github-light.svg').default as string, + dark: require('assets/social/github-dark.svg').default as string, }, }, - telegram: require('./assets/telegram.svg').default as string, + telegram: require('assets/social/telegram.svg').default as string, } as const; export const OAUTH_PROVIDERS = Object.keys(OAUTH_DATA); diff --git a/frontend/app/components/comment/__verification/_active/comment__verification_active.css b/frontend/app/components/comment/__verification/_active/comment__verification_active.css deleted file mode 100644 index 8f80033828..0000000000 --- a/frontend/app/components/comment/__verification/_active/comment__verification_active.css +++ /dev/null @@ -1,3 +0,0 @@ -.comment__verification_active { - background-image: url('./comment__verification_active.svg'); -} diff --git a/frontend/app/components/comment/__verification/_active/comment__verification_active.svg b/frontend/app/components/comment/__verification/_active/comment__verification_active.svg deleted file mode 100644 index aa745e2aca..0000000000 --- a/frontend/app/components/comment/__verification/_active/comment__verification_active.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/app/components/comment/__verification/_clickable/comment__verification_clickable.css b/frontend/app/components/comment/__verification/_clickable/comment__verification_clickable.css deleted file mode 100644 index c406abfcdc..0000000000 --- a/frontend/app/components/comment/__verification/_clickable/comment__verification_clickable.css +++ /dev/null @@ -1,3 +0,0 @@ -.comment__verification_clickable { - cursor: pointer; -} diff --git a/frontend/app/components/comment/__verification/comment__verification.css b/frontend/app/components/comment/__verification/comment__verification.css deleted file mode 100644 index 54e41e76e2..0000000000 --- a/frontend/app/components/comment/__verification/comment__verification.css +++ /dev/null @@ -1,13 +0,0 @@ -.comment__verification { - display: inline-block; - width: 12px; - height: 12px; - margin-left: 4px; - vertical-align: middle; - background: url('./comment__verification.svg') center no-repeat; - background-size: cover; - - &:hover { - opacity: 0.75; - } -} diff --git a/frontend/app/components/comment/__verification/comment__verification.svg b/frontend/app/components/comment/__verification/comment__verification.svg deleted file mode 100644 index aafbd7989c..0000000000 --- a/frontend/app/components/comment/__verification/comment__verification.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/app/components/comment/comment.module.css b/frontend/app/components/comment/comment.module.css new file mode 100644 index 0000000000..1ed65bac1d --- /dev/null +++ b/frontend/app/components/comment/comment.module.css @@ -0,0 +1,29 @@ +.user { + display: flex; + align-items: center; +} + +.user > * + * { + margin-left: 4px; +} + +.verificationButton { + display:flex; + align-items: center; + padding: 2px; + width: 16px; + height: 16px; +} + +.verificationIcon { + color: var(--color29); +} + +.verificationIconInactive { + color: var(--color1); + transition: opacity 0.15s; + + &:hover { + opacity: 0.75; + } +} diff --git a/frontend/app/components/comment/comment.test.tsx b/frontend/app/components/comment/comment.test.tsx index 8a48fc9531..2605bc840d 100644 --- a/frontend/app/components/comment/comment.test.tsx +++ b/frontend/app/components/comment/comment.test.tsx @@ -1,16 +1,8 @@ import { h } from 'preact'; import { mount } from 'enzyme'; - -jest.mock('react-intl', () => { - const messages = require('locales/en.json'); - const reactIntl = jest.requireActual('react-intl'); - const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {}); - - return { - ...reactIntl, - useIntl: () => intlProvider.state.intl, - }; -}); +import '@testing-library/jest-dom'; +import { screen } from '@testing-library/preact'; +import { render } from 'tests/utils'; import { useIntl, IntlProvider } from 'react-intl'; @@ -21,46 +13,89 @@ import { sleep } from 'utils/sleep'; import { Comment, CommentProps } from './comment'; -function mountComment(props: CommentProps) { - function Wrapper(updateProps: Partial = {}) { - const intl = useIntl(); - - return ( - - - - ); - } +function CommentWithIntl(props: CommentProps) { + return ; +} - return mount(); +// @depricated +function mountComment(props: CommentProps) { + return mount( + + + + ); } -const DefaultProps: Partial = { - post_info: { - read_only: false, - } as PostInfo, - view: 'main', - data: { - text: 'test comment', - vote: 0, +function getDefaultProps() { + return { + post_info: { + read_only: false, + } as PostInfo, + view: 'main', + data: { + text: 'test comment', + vote: 0, + user: { + id: 'someone', + name: 'username', + picture: 'somepicture-url', + }, + time: new Date().toString(), + locator: { + url: 'somelocatorurl', + site: 'remark', + }, + } as CommentType, user: { - id: 'someone', + admin: false, + id: 'testuser', picture: 'somepicture-url', - }, - time: new Date().toString(), - locator: { - url: 'somelocatorurl', - site: 'remark', - }, - } as CommentType, - user: { - admin: false, - id: 'testuser', - picture: 'somepicture-url', - } as User, -} as CommentProps; + } as User, + } as CommentProps & { user: User }; +} +const DefaultProps = getDefaultProps(); describe('', () => { + it('should render patreon subscriber icon', async () => { + const props = getDefaultProps() as CommentProps; + props.data.user.paid_sub = true; + + render(); + const patreonSubscriberIcon = await screen.findByAltText('Patreon Paid Subscriber'); + expect(patreonSubscriberIcon).toBeVisible(); + expect(patreonSubscriberIcon.tagName).toBe('IMG'); + }); + + 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(); + render(); + expect(screen.queryByTitle('Verified user')).not.toBeInTheDocument(); + }); + + it('should render verification button for admin', () => { + const props = getDefaultProps(); + 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.data.user.verified = true; + render(); + expect(screen.queryByTitle('Verified user')).toBeVisible(); + }); + }); + describe('voting', () => { it('should be disabled for an anonymous user', () => { const wrapper = mountComment({ ...DefaultProps, user: { id: 'anonymous_1' } } as CommentProps); @@ -226,54 +261,24 @@ describe('', () => { expect(controls.at(0).text()).toEqual('Hide'); }); - it('verification badge clickable for admin', () => { - const element = mountComment({ ...DefaultProps, user: { ...DefaultProps.user, admin: true } } as CommentProps); - - const controls = element.find('.comment__verification').first(); - expect(controls.hasClass('comment__verification_clickable')).toEqual(true); - }); - - it('verification badge not clickable for regular user', () => { - const element = mountComment({ - ...DefaultProps, - data: { ...DefaultProps.data, user: { ...DefaultProps.data!.user, verified: true } }, - } as CommentProps); - - const controls = element.find('.comment__verification').first(); - expect(controls.hasClass('comment__verification_clickable')).toEqual(false); - }); - - it('should be editable', () => { + it('should be editable', async () => { StaticStore.config.edit_duration = 300; - const initTime = new Date().toString(); - const changedTime = new Date(Date.now() + 10 * 1000).toString(); - const props = { - ...DefaultProps, - user: DefaultProps.user as User, - data: { - ...DefaultProps.data, - id: '100', - user: DefaultProps.user as User, - vote: 1, - time: initTime, - delete: false, - orig: 'test', - } as CommentType, - repliesCount: 0, - } as CommentProps; - const component = mountComment(props); - const comment = component.find(Comment); - - expect((comment.state('editDeadline') as Date).getTime()).toBe( - new Date(new Date(initTime).getTime() + 300 * 1000).getTime() - ); - - component.setProps({ data: { ...props.data, time: changedTime } }); + const props = getDefaultProps(); + props.repliesCount = 0; + props.user!.id = '100'; + props.data.user.id = '100'; + Object.assign(props.data, { + id: '101', + vote: 1, + time: new Date().toString(), + delete: false, + orig: 'test', + }); - expect((comment.state('editDeadline') as Date).getTime()).toBe( - new Date(new Date(changedTime).getTime() + 300 * 1000).getTime() - ); + render(); + // it can be less than 300 due to test checks time + expect(['299', '300']).toContain(screen.getByRole('timer').innerText); }); it('should not be editable', () => { diff --git a/frontend/app/components/comment/comment.tsx b/frontend/app/components/comment/comment.tsx index 991ddeb32e..a39d1dcbfa 100644 --- a/frontend/app/components/comment/comment.tsx +++ b/frontend/app/components/comment/comment.tsx @@ -1,5 +1,7 @@ import { h, JSX, Component, createRef, ComponentType } from 'preact'; +import { FormattedMessage, IntlShape, defineMessages } from 'react-intl'; import b from 'bem-react-helper'; +import clsx from 'clsx'; import { getHandleClickProps } from 'common/accessibility'; import { COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants'; @@ -15,13 +17,14 @@ import { CommentFormProps } from 'components/comment-form'; import { Avatar } from 'components/avatar'; import { Button } from 'components/button'; import { Countdown } from 'components/countdown'; +import { VerificationIcon } from 'components/icons/verification'; import { getPreview, uploadImage } from 'common/api'; import { postMessageToParent } from 'utils/post-message'; -import { FormattedMessage, IntlShape, defineMessages } from 'react-intl'; import { getVoteMessage, VoteMessagesTypes } from './getVoteMessage'; import { getBlockingDurations } from './getBlockingDurations'; import { boundActions } from './connected-comment'; +import styles from './comment.module.css'; import './styles'; export type CommentProps = { @@ -79,7 +82,7 @@ export class Comment extends Component { /** comment text node. Used in comment text copying */ textNode = createRef(); - updateState = (props: CommentProps) => { + updateState(props: CommentProps) { const newState: Partial = { scoreDelta: props.data.vote, cachedScore: props.data.score, @@ -103,7 +106,7 @@ export class Comment extends Component { } return newState; - }; + } state = { renderDummy: typeof this.props.inView === 'boolean' ? !this.props.inView : false, @@ -595,26 +598,37 @@ export class Comment extends Component { )} - {props.view !== 'user' && ( - this.toggleUserInfoVisibility()} className="comment__username"> - {o.user.name} - - )} - {isAdmin && props.view !== 'user' && ( - - )} - {!isAdmin && !!o.user.verified && props.view !== 'user' && ( - - )} + + {props.view !== 'user' && ( + this.toggleUserInfoVisibility()} className="comment__username"> + {o.user.name} + + )} + {isAdmin && props.view !== 'user' && ( + + + + )} + {!isAdmin && !!o.user.verified && props.view !== 'user' && ( + + )} + {o.user.paid_sub && ( + + )} + {getLocalDatetime(this.props.intl, o.time)} @@ -878,4 +892,8 @@ const messages = defineMessages({ id: 'comment.time', defaultMessage: '{day} at {time}', }, + paidPatreon: { + id: 'comment.paid-patreon', + defaultMessage: 'Patreon Paid Subscriber', + }, }); diff --git a/frontend/app/components/comment/styles.ts b/frontend/app/components/comment/styles.ts index decdbbf4b2..215ec5928d 100644 --- a/frontend/app/components/comment/styles.ts +++ b/frontend/app/components/comment/styles.ts @@ -26,10 +26,6 @@ import './__time/comment__time.css'; import './__user-id/comment__user-id.css'; import './__username/comment__username.css'; -import './__verification/comment__verification.css'; -import './__verification/_active/comment__verification_active.css'; -import './__verification/_clickable/comment__verification_clickable.css'; - import './__vote/comment__vote.css'; import './__vote/_disabled/comment__vote_disabled.css'; import './__vote/_selected/comment__vote_selected.css'; diff --git a/frontend/app/components/countdown/index.tsx b/frontend/app/components/countdown/index.tsx index 346b7ac404..d6ebc38732 100644 --- a/frontend/app/components/countdown/index.tsx +++ b/frontend/app/components/countdown/index.tsx @@ -56,6 +56,6 @@ export class Countdown extends Component { }, 1000); } render(props: Props) { - return ; + return ; } } diff --git a/frontend/app/components/icons/verification.spec.tsx b/frontend/app/components/icons/verification.spec.tsx new file mode 100644 index 0000000000..b07c260a1e --- /dev/null +++ b/frontend/app/components/icons/verification.spec.tsx @@ -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 { VerificationIcon } from './verification'; + +describe('', () => { + it('should be rendered with default size', async () => { + render(); + expect(await screen.findByTitle('icon')).toHaveAttribute('width', '12'); + expect(await screen.findByTitle('icon')).toHaveAttribute('height', '12'); + }); + 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/verification.tsx b/frontend/app/components/icons/verification.tsx new file mode 100644 index 0000000000..f7a52f30d1 --- /dev/null +++ b/frontend/app/components/icons/verification.tsx @@ -0,0 +1,22 @@ +import { h, JSX } from 'preact'; + +type Props = JSX.SVGAttributes & { size?: number }; + +export function VerificationIcon({ size = 12, ...props }: Props) { + return ( + + + + + ); +} diff --git a/frontend/app/components/verified.tsx b/frontend/app/components/verified.tsx deleted file mode 100644 index 7c9d265ad2..0000000000 --- a/frontend/app/components/verified.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export function Verified() { - return ( - - - - - ); -} diff --git a/frontend/app/styles/global.css b/frontend/app/styles/global.css index 1ab849bb8d..102f298435 100644 --- a/frontend/app/styles/global.css +++ b/frontend/app/styles/global.css @@ -27,6 +27,7 @@ button { transition-timing-function: linear; transition-duration: 150ms; appearance: none; + cursor: pointer; } #remark42 { diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 3cf7634b14..7a41628578 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -195,7 +195,7 @@ module.exports = (_, { mode, analyze }) => { options: { name: '[name].[ext]', publicPath: PUBLIC_PATH, - limit: 1200, + limit: false, }, }, };