From 088397ead57f423ea02140403aa38b8f34a84927 Mon Sep 17 00:00:00 2001 From: Paul Mineev <paul@mineev.me> Date: Sun, 10 Apr 2022 11:54:04 -0700 Subject: [PATCH 1/2] add icon for paid patreon sub --- .../assets => assets/social}/dev.svg | 0 .../assets => assets/social}/facebook.svg | 0 .../assets => assets/social}/github-dark.svg | 0 .../assets => assets/social}/github-light.svg | 0 .../assets => assets/social}/google.svg | 0 .../assets => assets/social}/microsoft.svg | 0 .../assets => assets/social}/patreon.svg | 0 .../assets => assets/social}/telegram.svg | 0 .../assets => assets/social}/twitter.svg | 0 .../assets => assets/social}/yandex.svg | 0 frontend/app/common/types.ts | 1 + .../auth/components/oauth.consts.ts | 20 +-- .../app/components/comment/comment.module.css | 8 ++ .../app/components/comment/comment.test.tsx | 135 ++++++++---------- frontend/app/components/comment/comment.tsx | 58 +++++--- frontend/app/components/countdown/index.tsx | 2 +- frontend/webpack.config.js | 2 +- 17 files changed, 120 insertions(+), 106 deletions(-) rename frontend/app/{components/auth/components/assets => assets/social}/dev.svg (100%) rename frontend/app/{components/auth/components/assets => assets/social}/facebook.svg (100%) rename frontend/app/{components/auth/components/assets => assets/social}/github-dark.svg (100%) rename frontend/app/{components/auth/components/assets => assets/social}/github-light.svg (100%) rename frontend/app/{components/auth/components/assets => assets/social}/google.svg (100%) rename frontend/app/{components/auth/components/assets => assets/social}/microsoft.svg (100%) rename frontend/app/{components/auth/components/assets => assets/social}/patreon.svg (100%) rename frontend/app/{components/auth/components/assets => assets/social}/telegram.svg (100%) rename frontend/app/{components/auth/components/assets => assets/social}/twitter.svg (100%) rename frontend/app/{components/auth/components/assets => assets/social}/yandex.svg (100%) create mode 100644 frontend/app/components/comment/comment.module.css 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/comment.module.css b/frontend/app/components/comment/comment.module.css new file mode 100644 index 0000000000..9648eabd6c --- /dev/null +++ b/frontend/app/components/comment/comment.module.css @@ -0,0 +1,8 @@ +.user { + display: flex; + align-items: center; +} + +.user > * + * { + margin-left: 4px; +} diff --git a/frontend/app/components/comment/comment.test.tsx b/frontend/app/components/comment/comment.test.tsx index 8a48fc9531..c0425dcd5e 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,58 @@ import { sleep } from 'utils/sleep'; import { Comment, CommentProps } from './comment'; -function mountComment(props: CommentProps) { - function Wrapper(updateProps: Partial<CommentProps> = {}) { - const intl = useIntl(); - - return ( - <IntlProvider locale="en" messages={enMessages}> - <Comment {...props} {...updateProps} intl={intl} /> - </IntlProvider> - ); - } +function CommentWithIntl(props: CommentProps) { + return <Comment {...props} intl={useIntl()} />; +} - return mount(<Wrapper />); +// @depricated +function mountComment(props: CommentProps) { + return mount( + <IntlProvider locale="en" messages={enMessages}> + <CommentWithIntl {...props} /> + </IntlProvider> + ); } -const DefaultProps: Partial<CommentProps> = { - 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', + picture: 'somepicture-url', + } as User, + 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; +} +const DefaultProps = getDefaultProps(); describe('<Comment />', () => { + it('should render patreon subscriber icon', async () => { + const props = getDefaultProps() as CommentProps; + props.data.user.paid_sub = true; + + render(<CommentWithIntl {...props} />); + const patreonSubscriberIcon = await screen.findByAltText('Patreon Paid Subscriber'); + expect(patreonSubscriberIcon).toBeVisible(); + expect(patreonSubscriberIcon.tagName).toBe('IMG'); + }); + describe('voting', () => { it('should be disabled for an anonymous user', () => { const wrapper = mountComment({ ...DefaultProps, user: { id: 'anonymous_1' } } as CommentProps); @@ -243,37 +247,24 @@ describe('<Comment />', () => { 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(<CommentWithIntl {...props} />); + // 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..d6e02a8a5b 100644 --- a/frontend/app/components/comment/comment.tsx +++ b/frontend/app/components/comment/comment.tsx @@ -22,6 +22,7 @@ import { getVoteMessage, VoteMessagesTypes } from './getVoteMessage'; import { getBlockingDurations } from './getBlockingDurations'; import { boundActions } from './connected-comment'; +import style from './comment.module.css'; import './styles'; export type CommentProps = { @@ -79,7 +80,7 @@ export class Comment extends Component<CommentProps, State> { /** comment text node. Used in comment text copying */ textNode = createRef<HTMLDivElement>(); - updateState = (props: CommentProps) => { + updateState(props: CommentProps) { const newState: Partial<State> = { scoreDelta: props.data.vote, cachedScore: props.data.score, @@ -103,7 +104,7 @@ export class Comment extends Component<CommentProps, State> { } return newState; - }; + } state = { renderDummy: typeof this.props.inView === 'boolean' ? !this.props.inView : false, @@ -595,26 +596,35 @@ export class Comment extends Component<CommentProps, State> { <Avatar url={o.user.picture} /> </div> )} - {props.view !== 'user' && ( - <button onClick={() => this.toggleUserInfoVisibility()} className="comment__username"> - {o.user.name} - </button> - )} - - {isAdmin && props.view !== 'user' && ( - <span - {...getHandleClickProps(this.toggleVerify)} - aria-label={intl.formatMessage(messages.toggleVerification)} - title={intl.formatMessage(o.user.verified ? messages.verifiedUser : messages.unverifiedUser)} - className={b('comment__verification', {}, { active: o.user.verified, clickable: true })} - /> - )} - {!isAdmin && !!o.user.verified && props.view !== 'user' && ( - <span - title={intl.formatMessage(messages.verifiedUser)} - className={b('comment__verification', {}, { active: true })} - /> - )} + <div className={style.user}> + {props.view !== 'user' && ( + <button onClick={() => this.toggleUserInfoVisibility()} className="comment__username"> + {o.user.name} + </button> + )} + {o.user.paid_sub && ( + <img + width={12} + height={12} + src={require('assets/social/patreon.svg').default} + alt={intl.formatMessage(messages.paidPatreon)} + /> + )} + {isAdmin && props.view !== 'user' && ( + <span + {...getHandleClickProps(this.toggleVerify)} + aria-label={intl.formatMessage(messages.toggleVerification)} + title={intl.formatMessage(o.user.verified ? messages.verifiedUser : messages.unverifiedUser)} + className={b('comment__verification', {}, { active: o.user.verified, clickable: true })} + /> + )} + {!isAdmin && !!o.user.verified && props.view !== 'user' && ( + <span + title={intl.formatMessage(messages.verifiedUser)} + className={b('comment__verification', {}, { active: true })} + /> + )} + </div> <a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__time"> {getLocalDatetime(this.props.intl, o.time)} @@ -878,4 +888,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/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<Props, State> { }, 1000); } render(props: Props) { - return <span {...exclude(props, 'time', 'onTimePassed')} ref={this.elemRef} />; + return <span role="timer" {...exclude(props, 'time', 'onTimePassed')} ref={this.elemRef} />; } } 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, }, }, }; From 174fc1c4cc529fccd35178e52e3e81062b0dad00 Mon Sep 17 00:00:00 2001 From: Paul Mineev <paul@mineev.me> Date: Sun, 10 Apr 2022 14:05:24 -0700 Subject: [PATCH 2/2] fix broken verify icon on admin view and optimize the icon --- .../_active/comment__verification_active.css | 3 -- .../_active/comment__verification_active.svg | 1 - .../comment__verification_clickable.css | 3 -- .../__verification/comment__verification.css | 13 ----- .../__verification/comment__verification.svg | 6 --- .../app/components/comment/comment.module.css | 21 ++++++++ .../app/components/comment/comment.test.tsx | 52 ++++++++++++------- frontend/app/components/comment/comment.tsx | 38 ++++++++------ frontend/app/components/comment/styles.ts | 4 -- .../components/icons/verification.spec.tsx | 18 +++++++ .../app/components/icons/verification.tsx | 22 ++++++++ frontend/app/components/verified.tsx | 17 ------ frontend/app/styles/global.css | 1 + 13 files changed, 116 insertions(+), 83 deletions(-) delete mode 100644 frontend/app/components/comment/__verification/_active/comment__verification_active.css delete mode 100644 frontend/app/components/comment/__verification/_active/comment__verification_active.svg delete mode 100644 frontend/app/components/comment/__verification/_clickable/comment__verification_clickable.css delete mode 100644 frontend/app/components/comment/__verification/comment__verification.css delete mode 100644 frontend/app/components/comment/__verification/comment__verification.svg create mode 100644 frontend/app/components/icons/verification.spec.tsx create mode 100644 frontend/app/components/icons/verification.tsx delete mode 100644 frontend/app/components/verified.tsx 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill="none" fill-rule="evenodd"><path fill="#4fbbd6" d="m5.823 14.822-1.28.206a.824.824 0 0 1-.9-.52l-.463-1.212a.824.824 0 0 0-.476-.476l-1.212-.462a.824.824 0 0 1-.52-.9l.206-1.281a.824.824 0 0 0-.175-.65L.185 8.52a.824.824 0 0 1 0-1.04l.818-1.006a.824.824 0 0 0 .175-.65L.972 4.542a.824.824 0 0 1 .52-.9l1.212-.463a.824.824 0 0 0 .476-.476l.462-1.212a.824.824 0 0 1 .9-.52l1.281.206a.824.824 0 0 0 .65-.175L7.48.185a.824.824 0 0 1 1.04 0l1.006.818a.824.824 0 0 0 .65.175l1.281-.206a.824.824 0 0 1 .9.52l.463 1.212c.084.22.257.392.476.476l1.212.462c.365.14.582.515.52.9l-.206 1.281a.824.824 0 0 0 .175.65l.818 1.007a.824.824 0 0 1 0 1.04l-.818 1.006a.824.824 0 0 0-.175.65l.206 1.281a.824.824 0 0 1-.52.9l-1.212.463a.824.824 0 0 0-.476.476l-.462 1.212a.824.824 0 0 1-.9.52l-1.281-.206a.824.824 0 0 0-.65.175l-1.007.818a.824.824 0 0 1-1.04 0l-1.006-.818a.824.824 0 0 0-.65-.175z"/><path stroke="#FFF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M4.755 8.252 7 10.5l4.495-4.495"/></g></svg> \ 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> - <g fill="none" fill-rule="evenodd"> - <path fill="#BBB" d="M5.823 14.822l-1.28.206a.824.824 0 0 1-.9-.52l-.463-1.212a.824.824 0 0 0-.476-.476l-1.212-.462a.824.824 0 0 1-.52-.9l.206-1.281a.824.824 0 0 0-.175-.65L.185 8.52a.824.824 0 0 1 0-1.04l.818-1.006a.824.824 0 0 0 .175-.65L.972 4.542a.824.824 0 0 1 .52-.9l1.212-.463a.824.824 0 0 0 .476-.476l.462-1.212a.824.824 0 0 1 .9-.52l1.281.206a.824.824 0 0 0 .65-.175L7.48.185a.824.824 0 0 1 1.04 0l1.006.818a.824.824 0 0 0 .65.175l1.281-.206a.824.824 0 0 1 .9.52l.463 1.212c.084.22.257.392.476.476l1.212.462c.365.14.582.515.52.9l-.206 1.281a.824.824 0 0 0 .175.65l.818 1.007a.824.824 0 0 1 0 1.04l-.818 1.006a.824.824 0 0 0-.175.65l.206 1.281a.824.824 0 0 1-.52.9l-1.212.463a.824.824 0 0 0-.476.476l-.462 1.212a.824.824 0 0 1-.9.52l-1.281-.206a.824.824 0 0 0-.65.175l-1.007.818a.824.824 0 0 1-1.04 0l-1.006-.818a.824.824 0 0 0-.65-.175z"/> - <path stroke="#FFF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M4.755 8.252L7 10.5l4.495-4.495"/> - </g> -</svg> diff --git a/frontend/app/components/comment/comment.module.css b/frontend/app/components/comment/comment.module.css index 9648eabd6c..1ed65bac1d 100644 --- a/frontend/app/components/comment/comment.module.css +++ b/frontend/app/components/comment/comment.module.css @@ -6,3 +6,24 @@ .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 c0425dcd5e..2605bc840d 100644 --- a/frontend/app/components/comment/comment.test.tsx +++ b/frontend/app/components/comment/comment.test.tsx @@ -37,8 +37,9 @@ function getDefaultProps() { vote: 0, user: { id: 'someone', + name: 'username', picture: 'somepicture-url', - } as User, + }, time: new Date().toString(), locator: { url: 'somelocatorurl', @@ -50,7 +51,7 @@ function getDefaultProps() { id: 'testuser', picture: 'somepicture-url', } as User, - } as CommentProps; + } as CommentProps & { user: User }; } const DefaultProps = getDefaultProps(); @@ -65,6 +66,36 @@ describe('<Comment />', () => { expect(patreonSubscriberIcon.tagName).toBe('IMG'); }); + describe('verification', () => { + it('should render active verification icon', () => { + const props = getDefaultProps(); + props.data.user.verified = true; + render(<CommentWithIntl {...props} />); + expect(screen.getByTitle('Verified user')).toBeVisible(); + }); + + it('should not render verification icon', () => { + const props = getDefaultProps(); + render(<CommentWithIntl {...props} />); + expect(screen.queryByTitle('Verified user')).not.toBeInTheDocument(); + }); + + it('should render verification button for admin', () => { + const props = getDefaultProps(); + props.user.admin = true; + render(<CommentWithIntl {...props} />); + expect(screen.getByTitle('Toggle verification')).toBeVisible(); + }); + + it('should render active verification icon for admin', () => { + const props = getDefaultProps(); + props.user.admin = true; + props.data.user.verified = true; + render(<CommentWithIntl {...props} />); + 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); @@ -230,23 +261,6 @@ describe('<Comment />', () => { 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', async () => { StaticStore.config.edit_duration = 300; diff --git a/frontend/app/components/comment/comment.tsx b/frontend/app/components/comment/comment.tsx index d6e02a8a5b..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,14 +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 style from './comment.module.css'; +import styles from './comment.module.css'; import './styles'; export type CommentProps = { @@ -596,12 +598,28 @@ export class Comment extends Component<CommentProps, State> { <Avatar url={o.user.picture} /> </div> )} - <div className={style.user}> + + <div className={styles.user}> {props.view !== 'user' && ( <button onClick={() => this.toggleUserInfoVisibility()} className="comment__username"> {o.user.name} </button> )} + {isAdmin && props.view !== 'user' && ( + <button + className={styles.verificationButton} + onClick={this.toggleVerify} + title={intl.formatMessage(messages.toggleVerification)} + > + <VerificationIcon + title={intl.formatMessage(o.user.verified ? messages.verifiedUser : messages.unverifiedUser)} + className={clsx(styles.verificationIcon, !o.user.verified && styles.verificationIconInactive)} + /> + </button> + )} + {!isAdmin && !!o.user.verified && props.view !== 'user' && ( + <VerificationIcon className={styles.verificationIcon} title={intl.formatMessage(messages.verifiedUser)} /> + )} {o.user.paid_sub && ( <img width={12} @@ -610,20 +628,6 @@ export class Comment extends Component<CommentProps, State> { alt={intl.formatMessage(messages.paidPatreon)} /> )} - {isAdmin && props.view !== 'user' && ( - <span - {...getHandleClickProps(this.toggleVerify)} - aria-label={intl.formatMessage(messages.toggleVerification)} - title={intl.formatMessage(o.user.verified ? messages.verifiedUser : messages.unverifiedUser)} - className={b('comment__verification', {}, { active: o.user.verified, clickable: true })} - /> - )} - {!isAdmin && !!o.user.verified && props.view !== 'user' && ( - <span - title={intl.formatMessage(messages.verifiedUser)} - className={b('comment__verification', {}, { active: true })} - /> - )} </div> <a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__time"> 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/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('<VerificationIcon />', () => { + it('should be rendered with default size', async () => { + render(<VerificationIcon title="icon" />); + 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(<VerificationIcon title="icon" size={16} />); + 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<SVGSVGElement> & { size?: number }; + +export function VerificationIcon({ size = 12, ...props }: Props) { + return ( + <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16" {...props}> + <path + fill="currentColor" + d="m5.823 14.822-1.28.206a.824.824 0 0 1-.9-.52l-.463-1.212a.824.824 0 0 0-.476-.476l-1.212-.462a.824.824 0 0 1-.52-.9l.206-1.281a.824.824 0 0 0-.175-.65L.185 8.52a.824.824 0 0 1 0-1.04l.818-1.006a.824.824 0 0 0 .175-.65L.972 4.542a.824.824 0 0 1 .52-.9l1.212-.463a.824.824 0 0 0 .476-.476l.462-1.212a.824.824 0 0 1 .9-.52l1.281.206a.824.824 0 0 0 .65-.175L7.48.185a.824.824 0 0 1 1.04 0l1.006.818a.824.824 0 0 0 .65.175l1.281-.206a.824.824 0 0 1 .9.52l.463 1.212c.084.22.257.392.476.476l1.212.462c.365.14.582.515.52.9l-.206 1.281a.824.824 0 0 0 .175.65l.818 1.007a.824.824 0 0 1 0 1.04l-.818 1.006a.824.824 0 0 0-.175.65l.206 1.281a.824.824 0 0 1-.52.9l-1.212.463a.824.824 0 0 0-.476.476l-.462 1.212a.824.824 0 0 1-.9.52l-1.281-.206a.824.824 0 0 0-.65.175l-1.007.818a.824.824 0 0 1-1.04 0l-1.006-.818a.824.824 0 0 0-.65-.175z" + /> + <path + fill="none" + stroke="#fff" + stroke-width="1.6" + stroke-linecap="round" + stroke-linejoin="round" + d="M4.755 8.252 7 10.5l4.495-4.495" + /> + </svg> + ); +} 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 ( - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> - <path - fill="#4fbbd6" - d="M5.823 14.822l-1.28.206a.824.824 0 0 1-.9-.52l-.463-1.212a.824.824 0 0 0-.476-.476l-1.212-.462a.824.824 0 0 1-.52-.9l.206-1.281a.824.824 0 0 0-.175-.65L.185 8.52a.824.824 0 0 1 0-1.04l.818-1.006a.824.824 0 0 0 .175-.65L.972 4.542a.824.824 0 0 1 .52-.9l1.212-.463a.824.824 0 0 0 .476-.476l.462-1.212a.824.824 0 0 1 .9-.52l1.281.206a.824.824 0 0 0 .65-.175L7.48.185a.824.824 0 0 1 1.04 0l1.006.818a.824.824 0 0 0 .65.175l1.281-.206a.824.824 0 0 1 .9.52l.463 1.212c.084.22.257.392.476.476l1.212.462c.365.14.582.515.52.9l-.206 1.281a.824.824 0 0 0 .175.65l.818 1.007a.824.824 0 0 1 0 1.04l-.818 1.006a.824.824 0 0 0-.175.65l.206 1.281a.824.824 0 0 1-.52.9l-1.212.463a.824.824 0 0 0-.476.476l-.462 1.212a.824.824 0 0 1-.9.52l-1.281-.206a.824.824 0 0 0-.65.175l-1.007.818a.824.824 0 0 1-1.04 0l-1.006-.818a.824.824 0 0 0-.65-.175z" - /> - <path - stroke="#fff" - stroke-width="1.6" - stroke-linecap="round" - stroke-linejoin="round" - d="M4.755 8.252L7 10.5l4.495-4.495" - /> - </svg> - ); -} 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 {