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 {