diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index f920fcf75c..2087f78222 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -165,10 +165,11 @@ jobs: run_install: false - name: Check bundle size - uses: andresz1/size-limit-action@v1 + uses: andresz1/size-limit-action@dd31dce7dcc72a041fd3e49abf0502b13fc4ce05 with: github_token: ${{ secrets.GITHUB_TOKEN }} directory: ./frontend/apps/remark42 + package_manager: pnpm test: name: Tests & Coverage diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index f7e9f6f5cd..0a7994e710 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -430,7 +430,7 @@ func (s *private) telegramSubscribeCtrl(w http.ResponseWriter, r *http.Request) var address, siteID string address, siteID, err := s.telegramService.CheckToken(queryToken, user.ID) if err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't set telegram for user", rest.ErrInternal) + rest.SendErrorJSON(w, r, http.StatusNotFound, err, "request is not verified yet", rest.ErrInternal) return } diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index df50100332..e94155c9cd 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -1238,8 +1238,8 @@ func TestRest_TelegramNotification(t *testing.T) { body, err = io.ReadAll(resp.Body) require.NoError(t, err) require.NoError(t, resp.Body.Close()) - require.Equal(t, http.StatusInternalServerError, resp.StatusCode, string(body)) - require.Equal(t, `{"code":0,"details":"can't set telegram for user","error":"not verified"}`+"\n", string(body)) + require.Equal(t, http.StatusNotFound, resp.StatusCode, string(body)) + require.Equal(t, `{"code":0,"details":"request is not verified yet","error":"not verified"}`+"\n", string(body)) mockTlgrm.notVerified = false diff --git a/frontend/apps/remark42/app/common/api.ts b/frontend/apps/remark42/app/common/api.ts index 2690e8a4e5..2eb2f0ed5a 100644 --- a/frontend/apps/remark42/app/common/api.ts +++ b/frontend/apps/remark42/app/common/api.ts @@ -78,6 +78,34 @@ export const uploadImage = (image: File): Promise => { /* Subscription methods */ +/** + * Start process of telegram subscription to updates + */ +export const telegramSubscribe = (): Promise<{ + bot: string; + token: string; +}> => apiFetcher.get('/telegram/subscribe'); + +/** + * Start process of telegram subscription to updates + * Example of error response: {"code":0,"details":"can't set telegram for user","error":"request is not verified yet"} + * Example of success response: {"address":"223211010","updated":true} + */ +export const telegramCurrentSubscribtion = ({ + token, +}: { + token: string; +}): Promise<{ + address: string; + updated: boolean; +}> => apiFetcher.get('/telegram/subscribe', { tkn: token }); + +/** + * Start process of telegram subscription to updates + * Example of success response: {"deleted":true} + */ +export const telegramUnsubcribe = (): Promise<{ deleted: boolean }> => apiFetcher.delete('/telegram'); + /** * Start process of email subscription to updates * @param emailAddress email for subscription diff --git a/frontend/apps/remark42/app/common/settings.ts b/frontend/apps/remark42/app/common/settings.ts index 118cd139ee..2b9011e79f 100644 --- a/frontend/apps/remark42/app/common/settings.ts +++ b/frontend/apps/remark42/app/common/settings.ts @@ -18,6 +18,7 @@ function includes(coll: ReadonlyArray, el: U): el is T { export const rawParams = parseQuery(); export const maxShownComments = parseNumber(rawParams.max_shown_comments) ?? MAX_SHOWN_ROOT_COMMENTS; export const isEmailSubscription = rawParams.show_email_subscription !== 'false'; +export const isTelegramSubscription = rawParams.show_telegram_subscription !== 'false'; export const isRssSubscription = rawParams.show_rss_subscription === undefined || rawParams.show_rss_subscription !== 'false'; export const theme = (rawParams.theme = includes(THEMES, rawParams.theme) ? rawParams.theme : THEMES[0]); diff --git a/frontend/apps/remark42/app/components/auth/auth.messsages.ts b/frontend/apps/remark42/app/components/auth/auth.messsages.ts index c990620c62..744f9e46d7 100644 --- a/frontend/apps/remark42/app/components/auth/auth.messsages.ts +++ b/frontend/apps/remark42/app/components/auth/auth.messsages.ts @@ -57,34 +57,6 @@ export const messages = defineMessages({ id: 'auth.submit', defaultMessage: 'Submit', }, - telegramLink: { - id: 'auth.telegram-link', - defaultMessage: 'by the link', - }, - telegramCheck: { - id: 'auth.telegram-check', - defaultMessage: 'Check', - }, - telegramQR: { - id: 'auth.telegram-qr', - defaultMessage: 'Telegram QR-code', - }, - telegramMessage1: { - id: 'auth.telegram-message-1', - defaultMessage: 'Open Telegram', - }, - telegramOptionalQR: { - id: 'auth.telegram-optional-qr', - defaultMessage: 'or by scanning the QR code', - }, - telegramMessage2: { - id: 'auth.telegram-message-2', - defaultMessage: 'and click “Start” there.', - }, - telegramMessage3: { - id: 'auth.telegram-message-3', - defaultMessage: 'Afterwards, click “Check” below.', - }, openProfile: { id: 'auth.open-profile', defaultMessage: 'Open My Profile', diff --git a/frontend/apps/remark42/app/components/auth/auth.module.css b/frontend/apps/remark42/app/components/auth/auth.module.css index 1c556f904e..594b40428c 100644 --- a/frontend/apps/remark42/app/components/auth/auth.module.css +++ b/frontend/apps/remark42/app/components/auth/auth.module.css @@ -195,15 +195,3 @@ color: var(--error-color); line-height: 1.2; } - -.telegramQR { - background-color: rgb(var(--white-color)); - box-sizing: border-box; - display: block; - margin: 12px auto; - padding: 10px; -} - -.telegram { - margin-bottom: 0; -} diff --git a/frontend/apps/remark42/app/components/auth/auth.tsx b/frontend/apps/remark42/app/components/auth/auth.tsx index 591ec553f0..1de4b8a2bf 100644 --- a/frontend/apps/remark42/app/components/auth/auth.tsx +++ b/frontend/apps/remark42/app/components/auth/auth.tsx @@ -4,9 +4,9 @@ import { useState, useRef } from 'preact/hooks'; import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; -import { BASE_URL, API_BASE } from 'common/constants.config'; import { setUser } from 'store/user/actions'; import { Input } from 'components/input'; +import { TelegramLink } from 'components/telegram/telegram-link'; import { CrossIcon } from 'components/icons/cross'; import { TextareaAutosize } from 'components/textarea-autosize'; import { Spinner } from 'components/spinner/spinner'; @@ -234,33 +234,12 @@ export function Auth() { -

- {intl.formatMessage(messages.telegramMessage1)}{' '} - - {intl.formatMessage(messages.telegramLink)} - - {window.screen.width >= 768 && ` ${intl.formatMessage(messages.telegramOptionalQR)}`}{' '} - {intl.formatMessage(messages.telegramMessage2)} -
- {intl.formatMessage(messages.telegramMessage3)} -

- {window.screen.width >= 768 && ( - {intl.formatMessage(messages.telegramQR)} - )} - {errorMessage &&
{errorMessage}
} - + ) : view === 'token' ? ( <> diff --git a/frontend/apps/remark42/app/components/button/button.tsx b/frontend/apps/remark42/app/components/button/button.tsx index b03ace54e7..693b7a29ca 100644 --- a/frontend/apps/remark42/app/components/button/button.tsx +++ b/frontend/apps/remark42/app/components/button/button.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import { h, JSX } from 'preact'; import { forwardRef } from 'preact/compat'; import b, { Mods, Mix } from 'bem-react-helper'; @@ -11,11 +12,17 @@ export type ButtonProps = Omit & { mods?: Mods; mix?: Mix; type?: string; + className?: string; }; export const Button = forwardRef( - ({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => ( - ) diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.module.css b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.module.css new file mode 100644 index 0000000000..2492bd5bfb --- /dev/null +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.module.css @@ -0,0 +1,15 @@ +.root { + display: flex; + flex-wrap: wrap; + flex-direction: column; + padding: 8px; + width: 250px; + box-sizing: border-box; + line-height: 1.2; + text-align: left; +} + +.preloader { + margin: 0 auto; + filter: invert(1); +} diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.test.tsx b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.test.tsx new file mode 100644 index 0000000000..46c0af808a --- /dev/null +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.test.tsx @@ -0,0 +1,197 @@ +import '@testing-library/jest-dom'; +import { fireEvent, screen, waitForElementToBeRemoved } from '@testing-library/preact'; + +import { render } from 'tests/utils'; +import * as api from 'common/api'; +import { user, anonymousUser } from '__stubs__/user'; +import { RequestError } from 'utils/errorUtils'; +import { sleep } from 'utils/sleep'; + +import { SubscribeByTelegram } from '.'; +import { StoreState } from 'store'; + +const initialStore = { + user, + theme: 'light', +} as const; + +describe('', () => { + beforeEach(() => { + jest + .spyOn(api, 'telegramSubscribe') + .mockClear() + .mockImplementation(async () => { + await sleep(10); + return { bot: 'foo_bot', token: 'foo_token' }; + }); + jest + .spyOn(api, 'telegramCurrentSubscribtion') + .mockClear() + .mockImplementation(async () => { + await sleep(10); + return { address: '223211010', updated: true }; + }); + jest + .spyOn(api, 'telegramUnsubcribe') + .mockClear() + .mockImplementation(async () => { + await sleep(10); + return { deleted: true }; + }); + sessionStorage.clear(); + }); + const createWrapper = (store: Partial = initialStore) => render(, store); + + it('should be rendered with disabled email button when user is anonymous', () => { + createWrapper({ ...initialStore, user: anonymousUser }); + + expect(screen.getByTitle('Available only for registered users')).toBeDisabled(); + }); + + it('should be rendered with enabled email button when user is logged in', () => { + createWrapper(); + + expect(screen.getByTitle('Subscribe by Telegram')).not.toBeDisabled(); + }); + + it('should show correct telegram link', async () => { + createWrapper(); + const button = screen.getByTitle('Subscribe by Telegram'); + + fireEvent.click(button); + + await screen.findByLabelText('Loading...'); + await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading...')); + + expect(await screen.findByText(/by the link/)).toHaveAttribute('href', 'https://t.me/foo_bot/?start=foo_token'); + }); + + it('should not do same API call to /subscribe twice during same session', async () => { + createWrapper(); + const button = screen.getByTitle('Subscribe by Telegram'); + + fireEvent.click(button); + + await screen.findByLabelText('Loading...'); + await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading...')); + + fireEvent.click(button); // close modal + fireEvent.click(button); + + expect(api.telegramSubscribe).toBeCalledTimes(1); + }); + + it('should subscribe', async () => { + createWrapper(); + const button = screen.getByTitle('Subscribe by Telegram'); + + fireEvent.click(button); + + const checkButton = await screen.findByText('Check'); + fireEvent.click(checkButton); + + await screen.findByLabelText('Loading...'); + await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading...')); + + expect(api.telegramCurrentSubscribtion).toBeCalledTimes(1); + expect(screen.getByText(/You have been subscribed/)).toBeInTheDocument(); + }); + + it('should subscribe and then unsubscribe', async () => { + createWrapper(); + const button = screen.getByTitle('Subscribe by Telegram'); + + fireEvent.click(button); + + const checkButton = await screen.findByText('Check'); + fireEvent.click(checkButton); + + const unsubscribeButton = await screen.findByText('Unsubscribe'); + fireEvent.click(unsubscribeButton); + + await screen.findByLabelText('Loading...'); + await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading...')); + + expect(api.telegramUnsubcribe).toBeCalledTimes(1); + expect(screen.getByText(/You have been unsubscribed/)).toBeInTheDocument(); + }); + + it('should subscribe, close window and then unsubscribe', async () => { + createWrapper(); + const button = screen.getByTitle('Subscribe by Telegram'); + + fireEvent.click(button); + + const checkButton = await screen.findByText('Check'); + fireEvent.click(checkButton); + + await screen.findByText('Unsubscribe'); // wait + fireEvent.click(button); // close + fireEvent.click(button); + + fireEvent.click(await screen.findByText('Unsubscribe')); + + await screen.findByLabelText('Loading...'); + await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading...')); + + expect(api.telegramUnsubcribe).toBeCalledTimes(1); + expect(screen.getByText(/You have been unsubscribed/)).toBeInTheDocument(); + }); + + it('should subscribe, unsubscribe and resubscribe', async () => { + createWrapper(); + const button = screen.getByTitle('Subscribe by Telegram'); + + fireEvent.click(button); + + const checkButton = await screen.findByText('Check'); + fireEvent.click(checkButton); + + const unsubscribeButton = await screen.findByText('Unsubscribe'); + fireEvent.click(unsubscribeButton); + + await screen.findByLabelText('Loading...'); + await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading...')); + + fireEvent.click(await screen.findByText('Resubscribe')); + fireEvent.click(await screen.findByText('Check')); + expect(api.telegramCurrentSubscribtion).toBeCalledTimes(2); + expect(await screen.findByText(/You have been subscribed/)).toBeInTheDocument(); + }); + + it('should show subscribed interface if user is already subscribed', async () => { + jest.spyOn(api, 'telegramSubscribe').mockImplementation(async () => { + await sleep(10); + const e = new RequestError('already subscribed', 17); + throw e; + }); + + createWrapper(); + const button = screen.getByTitle('Subscribe by Telegram'); + + fireEvent.click(button); + + await screen.findByLabelText('Loading...'); + await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading...')); + + expect(screen.getByText(/You have been subscribed/)).toBeInTheDocument(); + }); + + it('should show subscribed interface if user is already subscribed with 409 status code', async () => { + jest.spyOn(api, 'telegramSubscribe').mockImplementation(async () => { + await sleep(10); + const e = new RequestError('Conflict.', 409); + throw e; + }); + + createWrapper(); + const button = screen.getByTitle('Subscribe by Telegram'); + + fireEvent.click(button); + + await screen.findByLabelText('Loading...'); + await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading...')); + + expect(screen.getByText(/You have been subscribed/)).toBeInTheDocument(); + }); +}); diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.tsx b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.tsx new file mode 100644 index 0000000000..37464023ce --- /dev/null +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/comment-form__subscribe-by-telegram.tsx @@ -0,0 +1,175 @@ +import clsx from 'clsx'; +import { h, FunctionComponent, Fragment } from 'preact'; +import { useState, useEffect } from 'preact/hooks'; +import { useSelector } from 'react-redux'; +import { useIntl, defineMessages } from 'react-intl'; + +import { User } from 'common/types'; +import { StoreState } from 'store'; +import { FetcherError, RequestError, extractErrorMessageFromResponse } from 'utils/errorUtils'; +import { useTheme } from 'hooks/useTheme'; +import { useSessionStorage } from 'hooks/useSessionState'; +import { telegramSubscribe, telegramCurrentSubscribtion, telegramUnsubcribe } from 'common/api'; +import { Dropdown } from 'components/dropdown'; +import { isUserAnonymous } from 'utils/isUserAnonymous'; +import { TelegramLink } from 'components/telegram/telegram-link'; +import { Preloader } from 'components/preloader'; +import { Button } from 'components/button'; + +import styles from './comment-form__subscribe-by-telegram.module.css'; + +const messages = defineMessages({ + haveSubscribed: { + id: 'subscribeByTelegram.have-been-subscribed', + defaultMessage: 'You have been subscribed on updates by telegram', + }, + haveUnsubscribed: { + id: 'subscribeByTelegram.have-been-unsubscribed', + defaultMessage: 'You have been unsubscribed by telegram to updates', + }, + unsubscribe: { + id: 'subscribeByTelegram.unsubscribe', + defaultMessage: 'Unsubscribe', + }, + resubscribe: { + id: 'subscribeByTelegram.resubscribe', + defaultMessage: 'Resubscribe', + }, + subscribeByTelegram: { + id: 'subscribeByTelegram.subscribe-by-telegram', + defaultMessage: 'Subscribe by Telegram', + }, + onlyRegisteredUsers: { + id: 'subscribeByTelegram.only-registered-users', + defaultMessage: 'Available only for registered users', + }, + telegram: { + id: 'subscribeByTelegram.telegram', + defaultMessage: 'Telegram', + }, +}); + +type STEP = 'initial' | 'subscribed' | 'unsubscribed'; + +export const SubscribeByTelegramForm: FunctionComponent = () => { + const intl = useIntl(); + const [step, setStep] = useSessionStorage('telegram-subscription-step', 'initial'); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [telegram, setTelegram] = useSessionStorage<{ token: string; bot: string }>('telegram-subscription-telegram'); + + useEffect(() => { + const fetchTelegram = async () => { + setLoading(true); + try { + const { token, bot } = await telegramSubscribe(); + setTelegram({ token, bot }); + } catch (e) { + if ((e as RequestError).error === 'already subscribed') { + setStep('subscribed'); + return; + } + if ((e as RequestError).code === 409) { + setStep('subscribed'); + return; + } + setError(extractErrorMessageFromResponse(e as FetcherError, intl)); + } finally { + setLoading(false); + } + }; + if (telegram) { + return; + } + fetchTelegram(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [telegram]); + + const fetchTelegramSubscription = async (token: string) => { + try { + setTimeout(() => { + // to prevent dropdown closing + setLoading(true); + }, 0); + const { updated } = await telegramCurrentSubscribtion({ token }); + if (!updated) { + return; + } + setStep('subscribed'); + } catch (e) { + setError(extractErrorMessageFromResponse(e as FetcherError, intl)); + } finally { + setLoading(false); + } + }; + + const handleTelegramCheck = () => { + if (!telegram) { + return; + } + fetchTelegramSubscription(telegram.token); + }; + + const handleTelegramUnsubscribe = async () => { + try { + setTimeout(() => { + // to prevent dropdown closing + setLoading(true); + }, 0); + await telegramUnsubcribe(); + setStep('unsubscribed'); + } catch (e) { + setError(extractErrorMessageFromResponse(e as FetcherError, intl)); + } finally { + setLoading(false); + } + }; + + const handleTelegramResubscribe = async () => { + setStep('initial'); + }; + return ( +
+ {loading && } + {!loading && telegram && step === 'initial' && ( + + )} + {!loading && step === 'subscribed' && ( + <> +

{intl.formatMessage(messages.haveSubscribed)}

+ + + )} + {!loading && step === 'unsubscribed' && ( + <> +

{intl.formatMessage(messages.haveUnsubscribed)}

+ + + )} +
+ ); +}; + +export const SubscribeByTelegram: FunctionComponent = () => { + const theme = useTheme(); + const intl = useIntl(); + const user = useSelector(({ user }) => user); + const isAnonymous = isUserAnonymous(user); + const buttonTitle = intl.formatMessage(isAnonymous ? messages.onlyRegisteredUsers : messages.subscribeByTelegram); + + return ( + + + + ); +}; diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/index.ts b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/index.ts new file mode 100644 index 0000000000..b4055bea99 --- /dev/null +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-telegram/index.ts @@ -0,0 +1 @@ +export { SubscribeByTelegram } from './comment-form__subscribe-by-telegram'; diff --git a/frontend/apps/remark42/app/components/comment-form/comment-form.spec.tsx b/frontend/apps/remark42/app/components/comment-form/comment-form.spec.tsx index 7418477ff5..1214025aec 100644 --- a/frontend/apps/remark42/app/components/comment-form/comment-form.spec.tsx +++ b/frontend/apps/remark42/app/components/comment-form/comment-form.spec.tsx @@ -122,10 +122,42 @@ describe('', () => { expect(screen.getByText(/Subscribe by/)).toBeVisible(); expect(screen.getByTitle('Subscribe by RSS')).toBeVisible(); }); + it('renders Telegram subscription button', () => { + setup({ user }, { simple_view: true, telegram_notifications: true }); + expect(screen.getByText(/Subscribe by/)).toBeVisible(); + expect(screen.getByTitle('Subscribe by Telegram')).toBeVisible(); + }); + it('renders OR if telegram and RSS are enabled', () => { + setup({ user }, { simple_view: true, telegram_notifications: true, email_notifications: false }); + // I can not use testing-library to check 2 elements with OR exists, because both of them are in the same DOM element + const container = screen.getByText(/ or/); + const regex = /\bor\b/g; + const matchCount = (container.textContent?.match(regex) || []).length; + + expect(matchCount).toBe(1); + }); + it('renders 2 OR if telegram and RSS and email are enabled', () => { + setup({ user }, { simple_view: true, telegram_notifications: true, email_notifications: true }); + const container = screen.getByText(/ or/); + const regex = /\bor\b/g; + const matchCount = (container.textContent?.match(regex) || []).length; + + expect(matchCount).toBe(2); + }); }); it('renders without email subscription button when email_notifications disabled', () => { setup({ user }, { email_notifications: false }); - expect(screen.queryByText('Subscribe by RSS')).not.toBeInTheDocument(); + expect(screen.queryByTitle('Subscribe by Email')).not.toBeInTheDocument(); + }); + it('renders Telegram subscription button', () => { + setup({ user }, { telegram_notifications: true }); + expect(screen.getByText(/Subscribe by/)).toBeVisible(); + expect(screen.getByTitle('Subscribe by Telegram')).toBeVisible(); + }); + + it('renders without Telegram subscription button if telegram_notifications is false', () => { + setup({ user }, { telegram_notifications: false }); + expect(screen.queryByTitle('Subscribe by Telegram')).not.toBeInTheDocument(); }); }); diff --git a/frontend/apps/remark42/app/components/comment-form/comment-form.tsx b/frontend/apps/remark42/app/components/comment-form/comment-form.tsx index d4e8055b0b..7302f179de 100644 --- a/frontend/apps/remark42/app/components/comment-form/comment-form.tsx +++ b/frontend/apps/remark42/app/components/comment-form/comment-form.tsx @@ -14,6 +14,7 @@ import { TextareaAutosize } from 'components/textarea-autosize'; import { Auth } from 'components/auth'; import { SubscribeByEmail } from './__subscribe-by-email'; +import { SubscribeByTelegram } from './__subscribe-by-telegram'; import { SubscribeByRSS } from './__subscribe-by-rss'; import { MarkdownToolbar } from './markdown-toolbar'; @@ -376,9 +377,11 @@ export class CommentForm extends Component { renderSubscribeButtons = () => { const isEmailNotifications = StaticStore.config.email_notifications; const isEmailSubscription = isEmailNotifications && settings.isEmailSubscription; - const { isRssSubscription } = settings; + const isTelegramNotificationsEnabledOnBackend = StaticStore.config.telegram_notifications; + const isTelegramSubscription = isTelegramNotificationsEnabledOnBackend && settings.isTelegramSubscription; - if (!isRssSubscription && !isEmailSubscription) { + const { isRssSubscription } = settings; + if (!isRssSubscription && !isEmailSubscription && !isTelegramSubscription) { return null; } @@ -393,6 +396,13 @@ export class CommentForm extends Component { )} {isEmailSubscription && } + {(isRssSubscription && isTelegramSubscription) || (isEmailSubscription && isTelegramSubscription) ? ( + <> + {' '} + {' '} + + ) : null} + {isTelegramSubscription && } ); }; diff --git a/frontend/apps/remark42/app/components/root/_theme/_dark/root_theme_dark.css b/frontend/apps/remark42/app/components/root/_theme/_dark/root_theme_dark.css index 8fb009b054..476bfdbd26 100644 --- a/frontend/apps/remark42/app/components/root/_theme/_dark/root_theme_dark.css +++ b/frontend/apps/remark42/app/components/root/_theme/_dark/root_theme_dark.css @@ -1,6 +1,22 @@ .root_theme_dark { color: var(--color20); + & a { + color: var(--color20); + } + + & a:hover { + color: var(--color6); + } + + & a:visited { + color: var(--color39); + } + + & a:active { + color: var(--color39); + } + & .root__copyright { color: var(--color1); } diff --git a/frontend/apps/remark42/app/components/telegram/telegram-link.module.css b/frontend/apps/remark42/app/components/telegram/telegram-link.module.css new file mode 100644 index 0000000000..dbc2943959 --- /dev/null +++ b/frontend/apps/remark42/app/components/telegram/telegram-link.module.css @@ -0,0 +1,15 @@ +.telegram { + margin-bottom: 0; +} + +.telegramQR { + background-color: rgb(var(--white-color)); + box-sizing: border-box; + display: block; + margin: 12px auto; + padding: 10px; +} + +.button { + width: 100%; +} diff --git a/frontend/apps/remark42/app/components/telegram/telegram-link.spec.tsx b/frontend/apps/remark42/app/components/telegram/telegram-link.spec.tsx new file mode 100644 index 0000000000..088dabbfa3 --- /dev/null +++ b/frontend/apps/remark42/app/components/telegram/telegram-link.spec.tsx @@ -0,0 +1,76 @@ +import '@testing-library/jest-dom'; +import { screen } from '@testing-library/preact'; + +import { render } from 'tests/utils'; + +import { TelegramLink } from './telegram-link'; + +const handleSubmitSpy = jest.fn(); + +const defaultProps = { + bot: 'remark42_bot', + token: '718b5a43fb18136beb273e9261dd895578d00f63', + onSubmit: handleSubmitSpy, +}; + +describe('', () => { + let widthBackup: number; + beforeAll(() => { + widthBackup = window.screen.width; + }); + afterAll(() => { + Object.defineProperties(window.screen, { + width: { + writable: true, + value: widthBackup!, + }, + }); + }); + it('should render text', () => { + render(); + expect(screen.getByText(/Open Telegram/)).toBeInTheDocument(); + expect(screen.getByText(/by the link/)).toBeInTheDocument(); + expect(screen.getByText(/and click “Start” there./)).toBeInTheDocument(); + expect(screen.getByText(/Afterwards, click “Check” below./)).toBeInTheDocument(); + }); + it('should show QR code on desktop', () => { + Object.defineProperties(window.screen, { + width: { + writable: true, + value: 1000, + }, + }); + render(); + expect(screen.getByText(/or by scanning the QR code/)).toBeInTheDocument(); + expect(screen.getByAltText(/Telegram QR-code/)).toBeInTheDocument(); + }); + + it('should show contain correct QR src', () => { + Object.defineProperties(window.screen, { + width: { + writable: true, + value: 1000, + }, + }); + render(); + expect(screen.getByAltText(/Telegram QR-code/)).toHaveAttribute( + 'src', + `http://test.com/api/v1/qr/telegram?url=https://t.me/${defaultProps.bot}/?start=${defaultProps.token}` + ); + }); + it('should NOT show QR code on mobile', () => { + Object.defineProperties(window.screen, { + width: { + writable: true, + value: 500, + }, + }); + render(); + expect(screen.queryByAltText(/Telegram QR-code/)).not.toBeInTheDocument(); + }); + + it('should show error if any', () => { + render(); + expect(screen.getByText('Foo Error')).toBeInTheDocument(); + }); +}); diff --git a/frontend/apps/remark42/app/components/telegram/telegram-link.tsx b/frontend/apps/remark42/app/components/telegram/telegram-link.tsx new file mode 100644 index 0000000000..48bb1c96df --- /dev/null +++ b/frontend/apps/remark42/app/components/telegram/telegram-link.tsx @@ -0,0 +1,54 @@ +import clsx from 'clsx'; +import { h, Fragment, FunctionComponent } from 'preact'; +import { useIntl } from 'react-intl'; +import { messages } from './telegram.messages'; +import { BASE_URL, API_BASE } from 'common/constants.config'; +import { Button } from 'components/button'; + +import styles from './telegram-link.module.css'; + +export type TelegramLinkProps = { + bot: string; + token: string; + onSubmit: (evt: Event) => void; + errorMessage?: string | null; +}; + +export const TelegramLink: FunctionComponent = ({ bot, token, errorMessage, onSubmit }) => { + const intl = useIntl(); + const telegramLink = `https://t.me/${bot}/?start=${token}`; + return ( + <> +

+ {intl.formatMessage(messages.telegramMessage1)}{' '} + + {intl.formatMessage(messages.telegramLink)} + + {window.screen.width >= 768 && ` ${intl.formatMessage(messages.telegramOptionalQR)}`}{' '} + {intl.formatMessage(messages.telegramMessage2)} +
+ {intl.formatMessage(messages.telegramMessage3)} +

+ {window.screen.width >= 768 && ( + {intl.formatMessage(messages.telegramQR)} + )} + {errorMessage &&
{errorMessage}
} + + + ); +}; diff --git a/frontend/apps/remark42/app/components/telegram/telegram.messages.ts b/frontend/apps/remark42/app/components/telegram/telegram.messages.ts new file mode 100644 index 0000000000..7134f32395 --- /dev/null +++ b/frontend/apps/remark42/app/components/telegram/telegram.messages.ts @@ -0,0 +1,32 @@ +import { defineMessages } from 'react-intl'; + +export const messages = defineMessages({ + telegramMessage1: { + id: 'auth.telegram-message-1', + defaultMessage: 'Open Telegram', + }, + telegramLink: { + id: 'auth.telegram-link', + defaultMessage: 'by the link', + }, + telegramMessage2: { + id: 'auth.telegram-message-2', + defaultMessage: 'and click “Start” there.', + }, + telegramMessage3: { + id: 'auth.telegram-message-3', + defaultMessage: 'Afterwards, click “Check” below.', + }, + telegramOptionalQR: { + id: 'auth.telegram-optional-qr', + defaultMessage: 'or by scanning the QR code', + }, + telegramQR: { + id: 'auth.telegram-qr', + defaultMessage: 'Telegram QR-code', + }, + telegramCheck: { + id: 'auth.telegram-check', + defaultMessage: 'Check', + }, +}); diff --git a/frontend/apps/remark42/app/hooks/useSessionState.spec.ts b/frontend/apps/remark42/app/hooks/useSessionState.spec.ts new file mode 100644 index 0000000000..f5ea2fb62c --- /dev/null +++ b/frontend/apps/remark42/app/hooks/useSessionState.spec.ts @@ -0,0 +1,49 @@ +import { renderHook } from '@testing-library/preact-hooks'; + +import { useSessionStorage } from './useSessionState'; + +describe('useSessionStorage', () => { + it('should return a value and a setter', () => { + const { result } = renderHook(() => useSessionStorage('test', 0)); + expect(result.current!).toHaveLength(2); + expect(result.current![0]).toBe(0); + expect(result.current![1]).toBeInstanceOf(Function); + }); + + it('should return the initial value', () => { + const { result } = renderHook(() => useSessionStorage('test', 0)); + expect(result.current![0]).toBe(0); + }); + + it('should return the stored value', () => { + sessionStorage.setItem('test', JSON.stringify(1)); + const { result } = renderHook(() => useSessionStorage('test', 0)); + expect(result.current![0]).toBe(1); + }); + + it('should return the stored value if it is falsy', () => { + sessionStorage.setItem('test', JSON.stringify(false)); + const { result } = renderHook(() => useSessionStorage('test', 0)); + expect(result.current![0]).toBe(false); + }); + + it('should return the initial value if the stored value is not valid JSON', () => { + sessionStorage.setItem('test', 'not valid JSON'); + const { result } = renderHook(() => useSessionStorage('test', 0)); + expect(result.current![0]).toBe(0); + }); + + it('should return null if the stored value is null', () => { + // @ts-ignore + sessionStorage.setItem('test', null); + const { result } = renderHook(() => useSessionStorage('test', 0)); + expect(result.current![0]).toBe(null); + }); + + it('should return the initial value if the stored value is undefined', () => { + // @ts-ignore + sessionStorage.setItem('test', undefined); + const { result } = renderHook(() => useSessionStorage('test', 0)); + expect(result.current![0]).toBe(0); + }); +}); diff --git a/frontend/apps/remark42/app/hooks/useSessionState.ts b/frontend/apps/remark42/app/hooks/useSessionState.ts new file mode 100644 index 0000000000..09bf058cd4 --- /dev/null +++ b/frontend/apps/remark42/app/hooks/useSessionState.ts @@ -0,0 +1,23 @@ +import { useState, StateUpdater } from 'preact/hooks'; + +function useSessionStorage(key: string, initialValue?: T): [T, StateUpdater] { + const [storedValue, setStoredValue] = useState(() => { + const item = sessionStorage.getItem(key); + if (item === null) { + return initialValue; + } + try { + return JSON.parse(item); + } catch { + return initialValue; + } + }); + const setValue: typeof setStoredValue = (value) => { + const valueToStore = value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + sessionStorage.setItem(key, JSON.stringify(valueToStore)); + }; + return [storedValue, setValue]; +} + +export { useSessionStorage }; diff --git a/frontend/apps/remark42/app/locales/ar.json b/frontend/apps/remark42/app/locales/ar.json index e317577f22..eb99fa0ff4 100644 --- a/frontend/apps/remark42/app/locales/ar.json +++ b/frontend/apps/remark42/app/locales/ar.json @@ -108,6 +108,7 @@ "errors.7": "المستخدم محظور", "errors.8": "لا يمكن نشر تعليقات على هذه الصفحة. التعليقات مؤرشفة للقراءة فقط.", "errors.9": "فشل تعديل التعليق. فضلاً، عاود المحاولة.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "فشل الاتصال. فضلاً تأكد من اتصالك بالإنترنت.", "errors.forbidden": "ممنوع.", "errors.not-authorized": "غير مخوّل بالدخول.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "الموقع", "subscribeByRSS.thread": "سلسلة ردود", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "انسخ والصق الرمز المميز من البريد الإلكتروني", "token.expired": "الرمز انتهت صلاحيته", "token.invalid": "الرمز غير صالح", diff --git a/frontend/apps/remark42/app/locales/be.json b/frontend/apps/remark42/app/locales/be.json index 988bb83966..708b9c91c1 100644 --- a/frontend/apps/remark42/app/locales/be.json +++ b/frontend/apps/remark42/app/locales/be.json @@ -108,6 +108,7 @@ "errors.7": "Карыстальнік заблакаваны.", "errors.8": "Карыстальнік заблакаваны.", "errors.9": "Не атрымалася захаваць змяненні. Калі ласка, паспрабуйце яшчэ раз крыху пазней.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Няма адказу сервера. Праверце сваё злучэнне з інтэрнэтам або паспрабуйце яшчэ раз крыху пазней.r", "errors.forbidden": "Забаронена.", "errors.not-authorized": "Не аўтарызаваны.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Сайт", "subscribeByRSS.thread": "Гутарка", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Скапіруйце і ўстаўце токен з электроннага ліста", "token.expired": "Час дзеяння токена сышоў", "token.invalid": "Токен несапраўдны", diff --git a/frontend/apps/remark42/app/locales/bg.json b/frontend/apps/remark42/app/locales/bg.json index d6a33ba993..25bc18c8fc 100644 --- a/frontend/apps/remark42/app/locales/bg.json +++ b/frontend/apps/remark42/app/locales/bg.json @@ -108,6 +108,7 @@ "errors.7": "Потребителя бе блокиран.", "errors.8": "Потребителя бе блокиран.", "errors.9": "Промяната на коментара е неуспешна. Моля, опитайте отново по-късно.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Неуспешно извличане. Моля, проверете вашата интернет връзка или опитайте отново по-късно", "errors.forbidden": "Забранено.", "errors.not-authorized": "Неоторизиран.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Сайт", "subscribeByRSS.thread": "Нишка", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Копирайте и поставете токена от имейла", "token.expired": "Изтекъл токен", "token.invalid": "Токенът е невалиден", diff --git a/frontend/apps/remark42/app/locales/bp.json b/frontend/apps/remark42/app/locales/bp.json index 6e5a16c755..f816c1b86f 100644 --- a/frontend/apps/remark42/app/locales/bp.json +++ b/frontend/apps/remark42/app/locales/bp.json @@ -108,6 +108,7 @@ "errors.7": "O usuário foi bloqueado.", "errors.8": "O usuário foi bloqueado.", "errors.9": "A alteração do comentário falhou. Tente novamente um pouco mais tarde.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Falha ao buscar. Verifique sua conexão com a Internet ou tente novamente mais tarde", "errors.forbidden": "Proibido.", "errors.not-authorized": "Não autorizado.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Site", "subscribeByRSS.thread": "Tópico", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Copie e cole o token do e-mail", "token.expired": "O token expirou", "token.invalid": "Token inválido", diff --git a/frontend/apps/remark42/app/locales/cs.json b/frontend/apps/remark42/app/locales/cs.json index dc0075c040..b92afbf2ec 100644 --- a/frontend/apps/remark42/app/locales/cs.json +++ b/frontend/apps/remark42/app/locales/cs.json @@ -108,6 +108,7 @@ "errors.7": "Uživatel byl zablokován..", "errors.8": "Nelze přidávat komentáře, protože jsou pouze pro čtení", "errors.9": "Editace komentáře se nezdařila. Zkuste to prosím později.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Nepodařilo se načíst. Zkontrolujte prosím své internetové připojení nebo to zkuste později.", "errors.forbidden": "Zakázáno.", "errors.not-authorized": "Nejste přihlášen.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Web", "subscribeByRSS.thread": "Vlákno", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Zkopírujte a vložte token z e-mailu", "token.expired": "Token expiroval", "token.invalid": "Token je neplatný", diff --git a/frontend/apps/remark42/app/locales/de.json b/frontend/apps/remark42/app/locales/de.json index fec7cbf2f2..a9f4e9bd9c 100644 --- a/frontend/apps/remark42/app/locales/de.json +++ b/frontend/apps/remark42/app/locales/de.json @@ -108,6 +108,7 @@ "errors.7": "Benutzer wurde gesperrt.", "errors.8": "Benutzer wurde gesperrt.", "errors.9": "Änderungen konnten nicht gespeichert werden. Bitte versuchen Sie es später noch einmal.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Abruf fehlgeschlagen. Bitte prüfen Sie Ihre Internetverbindung oder versuchen Sie es etwas später erneut", "errors.forbidden": "Verboten.", "errors.not-authorized": "Nicht autorisiert.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Site", "subscribeByRSS.thread": "Thread", "subscribeByRSS.title": "RSS-Feed", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Token aus der E-Mail kopieren und einfügen", "token.expired": "Token ist abgelaufen", "token.invalid": "Token ist ungültig", diff --git a/frontend/apps/remark42/app/locales/en.json b/frontend/apps/remark42/app/locales/en.json index 0a8e0fe5aa..542eab964c 100644 --- a/frontend/apps/remark42/app/locales/en.json +++ b/frontend/apps/remark42/app/locales/en.json @@ -108,6 +108,7 @@ "errors.7": "User has been blocked.", "errors.8": "Can't post comments on this page. Comments are read only.", "errors.9": "Comment changing failed. Please try again a bit later.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Failed to fetch. Please check your internet connection or try again a bit later", "errors.forbidden": "Forbidden.", "errors.not-authorized": "Not authorized.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Site", "subscribeByRSS.thread": "Thread", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Copy and paste the token from the email", "token.expired": "Token is expired", "token.invalid": "Token is invalid", diff --git a/frontend/apps/remark42/app/locales/es.json b/frontend/apps/remark42/app/locales/es.json index 46c8e1d76c..4e909a113e 100644 --- a/frontend/apps/remark42/app/locales/es.json +++ b/frontend/apps/remark42/app/locales/es.json @@ -108,6 +108,7 @@ "errors.7": "El usuario ha sido bloqueado.", "errors.8": "El usuario ha sido bloqueado.", "errors.9": "No se ha podido cambiar el comentario. Por favor vuelve a intentar más tarde.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "No se ha podido obtener. Por favor revisa tu conexión a internet o vuelve a intentar más tarde", "errors.forbidden": "Prohibido.", "errors.not-authorized": "No autorizado.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Sitio", "subscribeByRSS.thread": "Hilo", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Copie y pegue el token del mensaje del correo electrónico", "token.expired": "Token expirado", "token.invalid": "El token no es válido", diff --git a/frontend/apps/remark42/app/locales/fi.json b/frontend/apps/remark42/app/locales/fi.json index 0212cf4d3d..7a9471955d 100644 --- a/frontend/apps/remark42/app/locales/fi.json +++ b/frontend/apps/remark42/app/locales/fi.json @@ -108,6 +108,7 @@ "errors.7": "Käyttäjä on estetty.", "errors.8": "Käyttäjä on estetty.", "errors.9": "Kommentin vaihtaminen epäonnistui. Yritä uudelleen myöhemmin.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Nouto epäonnistui. Tarkista internet-yhteytesi tai yritä uudelleen myöhemmin.", "errors.forbidden": "Kieletty.", "errors.not-authorized": "Ei sallittu.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Sivusto", "subscribeByRSS.thread": "Teema", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Kopioi ja liitä tunniste sähköpostista", "token.expired": "Tunnus on vanhentunut", "token.invalid": "Token is invalid", diff --git a/frontend/apps/remark42/app/locales/fr.json b/frontend/apps/remark42/app/locales/fr.json index aafb5cdf5d..93f109e9ec 100644 --- a/frontend/apps/remark42/app/locales/fr.json +++ b/frontend/apps/remark42/app/locales/fr.json @@ -108,6 +108,7 @@ "errors.7": "L'utilisateur a été bloqué.", "errors.8": "L'utilisateur a été bloqué.", "errors.9": "La modification du commentaire a échoué. Veuillez réessayer un peu plus tard.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Impossible de récupérer les donnés. Veuillez vérifier votre connexion Internet ou réessayez plus tard", "errors.forbidden": "Interdit.", "errors.not-authorized": "Non autorisé.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Site", "subscribeByRSS.thread": "Fil d'actualités", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Copiez et collez le jeton depuis l'e-mail", "token.expired": "Le jeton a expiré", "token.invalid": "Le jeton n'est pas valide", diff --git a/frontend/apps/remark42/app/locales/it.json b/frontend/apps/remark42/app/locales/it.json index aaf55d42ce..08e0810d98 100644 --- a/frontend/apps/remark42/app/locales/it.json +++ b/frontend/apps/remark42/app/locales/it.json @@ -108,6 +108,7 @@ "errors.7": "L'utente è stato bloccato.", "errors.8": "Impossibile commentare su questa pagina, i commenti sono in sola lettura.", "errors.9": "Modifica del commento non riuscita. Riprova tra poco.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Aggiornamento fallito. Verifica la tua connessione a internet o riprova più tardi.", "errors.forbidden": "Vietato.", "errors.not-authorized": "Non autorizzato.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Sito", "subscribeByRSS.thread": "Argomento", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Copiare e incollare il token dall'e-mail", "token.expired": "Il token è scaduto", "token.invalid": "Il token non è valido", diff --git a/frontend/apps/remark42/app/locales/ja.json b/frontend/apps/remark42/app/locales/ja.json index ed47574fd9..9fb2f55c5c 100644 --- a/frontend/apps/remark42/app/locales/ja.json +++ b/frontend/apps/remark42/app/locales/ja.json @@ -108,6 +108,7 @@ "errors.7": "ユーザーはブロックされました。", "errors.8": "ユーザーはブロックされました。", "errors.9": "コメントの変更に失敗しました。しばらくしてからもう一度お試しください。", + "errors.conflict": "Conflict.", "errors.failed-fetch": "取得できませんでした。インターネット接続を確認するか、しばらくしてからもう一度お試しください。", "errors.forbidden": "禁止されています.", "errors.not-authorized": "認証されていません。", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "サイト", "subscribeByRSS.thread": "スレッド", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "メールからトークンをコピーして貼り付ける", "token.expired": "トークンの有効期限が切れています", "token.invalid": "トークンが無効です", diff --git a/frontend/apps/remark42/app/locales/ko.json b/frontend/apps/remark42/app/locales/ko.json index ea054a6ff5..0eca6cf344 100644 --- a/frontend/apps/remark42/app/locales/ko.json +++ b/frontend/apps/remark42/app/locales/ko.json @@ -108,6 +108,7 @@ "errors.7": "사용자가 차단되었습니다.", "errors.8": "사용자가 차단되었습니다.", "errors.9": "댓글 변경이 실패했습니다. 잠시 후 다시 시도해 주세요.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "가져오기가 실패했습니다. 인터넷 연결을 확인하거나 잠시 후 다시 시도해 주세요.", "errors.forbidden": "허용되지 않습니다.", "errors.not-authorized": "권한이 없습니다.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "사이트", "subscribeByRSS.thread": "스레드", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "이메일에서 토큰을 복사 및 붙여넣기", "token.expired": "토큰이 만료되었습니다", "token.invalid": "토큰이 유효하지 않습니다", diff --git a/frontend/apps/remark42/app/locales/pl.json b/frontend/apps/remark42/app/locales/pl.json index 09b584dfd2..f0efd38eea 100644 --- a/frontend/apps/remark42/app/locales/pl.json +++ b/frontend/apps/remark42/app/locales/pl.json @@ -108,6 +108,7 @@ "errors.7": "Użytkownik został zablokowany.", "errors.8": "Użytkownik został zablokowany.", "errors.9": "Edycja komentarza nie powiodła się. Spróbuj ponownie później.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Nie udało się pobrać. Sprawdź swoje połączenie internetowe i spróbuj ponownie później", "errors.forbidden": "Zabronione.", "errors.not-authorized": "Nie jesteś upoważniony.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Strona", "subscribeByRSS.thread": "Wątek", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Skopiuj i wklej token z wiadomości e-mail", "token.expired": "Wygasły token", "token.invalid": "Token jest nieprawidłowy.", diff --git a/frontend/apps/remark42/app/locales/ru.json b/frontend/apps/remark42/app/locales/ru.json index 0fe4eff498..ee769d1b7c 100644 --- a/frontend/apps/remark42/app/locales/ru.json +++ b/frontend/apps/remark42/app/locales/ru.json @@ -108,6 +108,7 @@ "errors.7": "Пользователь был заблокирован.", "errors.8": "Пользователь был заблокирован.", "errors.9": "Не удалось сохранить изменения. Попробуйте еще раз позже.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Сервер не отвечает. Проверьте подключение к Интернету или попробуйте позже.", "errors.forbidden": "Запрещено.", "errors.not-authorized": "Не авторизован.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Сайт", "subscribeByRSS.thread": "Ветка", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Скопируйте токен из электронного письма", "token.expired": "Время действия токена истекло", "token.invalid": "Токен недействителен", diff --git a/frontend/apps/remark42/app/locales/th.json b/frontend/apps/remark42/app/locales/th.json index f8453bdb45..aa690f5ef4 100644 --- a/frontend/apps/remark42/app/locales/th.json +++ b/frontend/apps/remark42/app/locales/th.json @@ -108,6 +108,7 @@ "errors.7": "ผู้ใช้ถูกบล็อก", "errors.8": "ไม่สามารถแสดงความคิดเห็นในหน้านี้ สำหรับอ่านเท่านั้น", "errors.9": "การเปลี่ยนความคิดเห็นล้มเหลว โปรดลองอีกครั้งในภายหลัง", + "errors.conflict": "Conflict.", "errors.failed-fetch": "ดึงข้อมูลไม่สำเร็จ โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณหรือลองอีกครั้งในภายหลัง", "errors.forbidden": "ปฏิเสธ", "errors.not-authorized": "ไม่มีสิทธิ์เข้าถึง", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "เว็บไซต์", "subscribeByRSS.thread": "เทรด", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "คัดลอกและวางโทเค็นจากอีเมล", "token.expired": "โทเค็นหมดอายุ", "token.invalid": "โทเค็นไม่ถูกต้อง", diff --git a/frontend/apps/remark42/app/locales/tr.json b/frontend/apps/remark42/app/locales/tr.json index 095b931529..0e272ab7ad 100644 --- a/frontend/apps/remark42/app/locales/tr.json +++ b/frontend/apps/remark42/app/locales/tr.json @@ -108,6 +108,7 @@ "errors.7": "Kullanıcı engellendi.", "errors.8": "Kullanıcı engellendi.", "errors.9": "Yorum değiştirilemedi. Lütfen daha sonra tekrar deneyin.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "İçerik yüklenemedi. Lütfen internet bağlantınızı kontrol edin veya daha sonra tekrar deneyin.", "errors.forbidden": "İzin yok.", "errors.not-authorized": "Yetki yetersiz.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Site", "subscribeByRSS.thread": "Başlık", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "E-postadaki belirteci kopyalayıp yapıştırın", "token.expired": "Belirtecin süresi doldu", "token.invalid": "Belirteç geçersiz", diff --git a/frontend/apps/remark42/app/locales/ua.json b/frontend/apps/remark42/app/locales/ua.json index 2caff945fd..62a3be5869 100644 --- a/frontend/apps/remark42/app/locales/ua.json +++ b/frontend/apps/remark42/app/locales/ua.json @@ -108,6 +108,7 @@ "errors.7": "Користувач був заблокований.", "errors.8": "Користувач був заблокований.", "errors.9": "Не вдалося зберегти зміни.Спробуйте ще раз пізніше.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Немає відповіді від сервера. Перевірте ваше з’єднання з інтернетом або спробуйте пізніше.", "errors.forbidden": "Заборонено.", "errors.not-authorized": "Не авторизований.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Сайт", "subscribeByRSS.thread": "Гілка", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Скопіюйте та вставте токен з листа", "token.expired": "Час дії токена минув", "token.invalid": "Токен недійсний", diff --git a/frontend/apps/remark42/app/locales/vi.json b/frontend/apps/remark42/app/locales/vi.json index 55e895505a..1447ae0380 100644 --- a/frontend/apps/remark42/app/locales/vi.json +++ b/frontend/apps/remark42/app/locales/vi.json @@ -108,6 +108,7 @@ "errors.7": "Người dùng đã bị chặn.", "errors.8": "Người dùng đã bị chặn.", "errors.9": "Thay đổi bình luận không thành công. Một chút sau rồi thử lại nhé.", + "errors.conflict": "Conflict.", "errors.failed-fetch": "Nhận dữ liệu thất bại. Vui lòng kiểm tra lại đường truyền hoặc thử lại sau.", "errors.forbidden": "Bị cấm.", "errors.not-authorized": "Không được phép.", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "Trang", "subscribeByRSS.thread": "Chủ đề", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "Sao chép và dán token từ email", "token.expired": "token đã hết hạn", "token.invalid": "Token không hợp lệ", diff --git a/frontend/apps/remark42/app/locales/zh-tw.json b/frontend/apps/remark42/app/locales/zh-tw.json index fff002ee5e..909c99bd58 100644 --- a/frontend/apps/remark42/app/locales/zh-tw.json +++ b/frontend/apps/remark42/app/locales/zh-tw.json @@ -108,6 +108,7 @@ "errors.7": "使用者已被封鎖", "errors.8": "留言功能為唯獨模式,無法在此頁面新增留言。", "errors.9": "留言更改失敗,請稍後再嘗試。", + "errors.conflict": "Conflict.", "errors.failed-fetch": "請求失敗,請確認您的網路連線功能後再重新嘗試。", "errors.forbidden": "禁止的。", "errors.not-authorized": "未授權的。", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "網站", "subscribeByRSS.thread": "Thread", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "從電子郵件中復制並粘貼 token", "token.expired": "Token 已過期", "token.invalid": "無效的 Token", diff --git a/frontend/apps/remark42/app/locales/zh.json b/frontend/apps/remark42/app/locales/zh.json index 8cf5ce8f85..c366aea8c7 100644 --- a/frontend/apps/remark42/app/locales/zh.json +++ b/frontend/apps/remark42/app/locales/zh.json @@ -108,6 +108,7 @@ "errors.7": "用户已被封禁。", "errors.8": "用户已被封禁。", "errors.9": "评论更改失败,请稍后再试。", + "errors.conflict": "Conflict.", "errors.failed-fetch": "请求失败,请检查您的网络连接或稍候重试。", "errors.forbidden": "禁止", "errors.not-authorized": "未经授权。", @@ -152,6 +153,13 @@ "subscribeByRSS.site": "网站", "subscribeByRSS.thread": "线程", "subscribeByRSS.title": "RSS", + "subscribeByTelegram.have-been-subscribed": "You have been subscribed on updates by telegram", + "subscribeByTelegram.have-been-unsubscribed": "You have been unsubscribed by telegram to updates", + "subscribeByTelegram.only-registered-users": "Available only for registered users", + "subscribeByTelegram.resubscribe": "Resubscribe", + "subscribeByTelegram.subscribe-by-telegram": "Subscribe by Telegram", + "subscribeByTelegram.telegram": "Telegram", + "subscribeByTelegram.unsubscribe": "Unsubscribe", "token": "从电子邮件中复制并粘贴令牌", "token.expired": "令牌已过期", "token.invalid": "令牌无效", diff --git a/frontend/apps/remark42/app/styles/custom-properties.css b/frontend/apps/remark42/app/styles/custom-properties.css index 5836cdeadc..affc7bcc36 100644 --- a/frontend/apps/remark42/app/styles/custom-properties.css +++ b/frontend/apps/remark42/app/styles/custom-properties.css @@ -43,6 +43,7 @@ --color38: #ef0000; --color27: #f98989; --color26: #ffd7d7; + --color39: #6d4b8d; --color30: 204, 6, 6; --color12: 37, 158, 6; diff --git a/frontend/apps/remark42/app/typings/global.d.ts b/frontend/apps/remark42/app/typings/global.d.ts index 4094a7f733..8bb9d750a2 100644 --- a/frontend/apps/remark42/app/typings/global.d.ts +++ b/frontend/apps/remark42/app/typings/global.d.ts @@ -28,6 +28,10 @@ type RemarkConfig = { // if you set this param in 'false' you will get notifications email notifications as admin but your users // won't have interface for subscription show_email_subscription?: boolean; + // Optional, 'true' by default. Enables telegram subscription feature in interface when enable it from backend side, + // if you set this param in 'false' you will get telegram notifications as admin but your users + // won't have interface for subscription + show_telegram_subscription?: boolean; // Optional, 'true' by default. Enables RSS subscription feature in interface. show_rss_subscription?: boolean; // Optional, 'false' by default. Overrides the parameter from the backend minimized UI with basic info only. diff --git a/frontend/apps/remark42/package.json b/frontend/apps/remark42/package.json index 859ccafb06..c96ede8d0c 100644 --- a/frontend/apps/remark42/package.json +++ b/frontend/apps/remark42/package.json @@ -59,6 +59,7 @@ "@swc/jest": "^0.2.21", "@testing-library/jest-dom": "^5.16.4", "@testing-library/preact": "^3.2.2", + "@testing-library/preact-hooks": "^1.1.0", "@types/enzyme": "^3.10.12", "@types/eslint": "^8.4.5", "@types/jest": "^28.1.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 68e96deeab..0a12869724 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -31,6 +31,7 @@ importers: '@swc/jest': ^0.2.21 '@testing-library/jest-dom': ^5.16.4 '@testing-library/preact': ^3.2.2 + '@testing-library/preact-hooks': ^1.1.0 '@types/enzyme': ^3.10.12 '@types/eslint': ^8.4.5 '@types/jest': ^28.1.4 @@ -145,6 +146,7 @@ importers: '@swc/jest': 0.2.21_@swc+core@1.2.205 '@testing-library/jest-dom': 5.16.4 '@testing-library/preact': 3.2.2_preact@10.6.2 + '@testing-library/preact-hooks': 1.1.0_3jy4ehlhhntshptwdxmaawp6ue '@types/enzyme': 3.10.12 '@types/eslint': 8.4.5 '@types/jest': 28.1.5 @@ -2623,6 +2625,16 @@ packages: redent: 3.0.0 dev: true + /@testing-library/preact-hooks/1.1.0_3jy4ehlhhntshptwdxmaawp6ue: + resolution: {integrity: sha512-+JIor+NsOHkK3oIrwMDGKGHXTN0JJi462dBJlj4FNbGaDPTlctE6eu2ranWQirh7/FJMkWfzQCP+tk7jmY8ZrQ==} + peerDependencies: + '@testing-library/preact': ^2.0.0 + preact: ^10.4.8 + dependencies: + '@testing-library/preact': 3.2.2_preact@10.6.2 + preact: 10.6.2 + dev: true + /@testing-library/preact/3.2.2_preact@10.6.2: resolution: {integrity: sha512-mMPEp/9TOOqf3QqDHY02ieGFfRbi8fAxZvRifn+vOzrdNcCR1zchwPA6BvqXG3wAweRan4QJioYgEc1cePeC3g==} engines: {node: '>= 12'}