feat(ui): telegram user subscriptions

This commit is contained in:
Alexander Gusman
2023-07-24 12:29:55 -05:00
committed by Umputun
parent 366cc19c1b
commit 0ed7452e77
44 changed files with 929 additions and 73 deletions
+28
View File
@@ -78,6 +78,34 @@ export const uploadImage = (image: File): Promise<Image> => {
/* 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
@@ -18,6 +18,7 @@ function includes<T extends U, U>(coll: ReadonlyArray<T>, 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]);
@@ -57,34 +57,6 @@ export const messages = defineMessages<string>({
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',
@@ -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;
}
@@ -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() {
<CrossIcon />
</button>
</div>
<p className={clsx('telegram', styles.telegram)}>
{intl.formatMessage(messages.telegramMessage1)}{' '}
<a
target="_blank"
rel="noopener noreferrer"
href={`https://t.me/${telegramParamsRef.current.bot}/?start=${telegramParamsRef.current.token}`}
>
{intl.formatMessage(messages.telegramLink)}
</a>
{window.screen.width >= 768 && ` ${intl.formatMessage(messages.telegramOptionalQR)}`}{' '}
{intl.formatMessage(messages.telegramMessage2)}
<br />
{intl.formatMessage(messages.telegramMessage3)}
</p>
{window.screen.width >= 768 && (
<img
src={`${BASE_URL}${API_BASE}/qr/telegram?url=https://t.me/${telegramParamsRef.current.bot}/?start=${telegramParamsRef.current.token}`}
height="200"
width="200"
className={clsx('telegram-qr', styles.telegramQR)}
alt={intl.formatMessage(messages.telegramQR)}
/>
)}
{errorMessage && <div className={clsx('auth-error', styles.error)}>{errorMessage}</div>}
<Button key="submit" className="auth-submit" type="submit" onClick={handleTelegramSubmit}>
{intl.formatMessage(messages.telegramCheck)}
</Button>
<TelegramLink
onSubmit={handleTelegramSubmit}
errorMessage={errorMessage}
bot={telegramParamsRef.current.bot}
token={telegramParamsRef.current.token}
/>
</>
) : view === 'token' ? (
<>
@@ -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<JSX.HTMLAttributes, 'size' | 'className'> & {
mods?: Mods;
mix?: Mix;
type?: string;
className?: string;
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, theme, mods, mix, kind, type = 'button', size, ...props }, ref) => (
<button className={b('button', { mods: { kind, size, theme }, mix }, { ...mods })} type={type} {...props} ref={ref}>
({ children, theme, mods, mix, kind, type = 'button', size, className, ...props }, ref) => (
<button
className={clsx(b('button', { mods: { kind, size, theme }, mix }, { ...mods }), className)}
type={type}
{...props}
ref={ref}
>
{children}
</button>
)
@@ -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);
}
@@ -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('<SubscribeByTelegram />', () => {
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<StoreState> = initialStore) => render(<SubscribeByTelegram />, 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();
});
});
@@ -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<STEP>('telegram-subscription-step', 'initial');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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 (
<div className={clsx(styles.root)}>
{loading && <Preloader className={clsx(styles.preloader)} />}
{!loading && telegram && step === 'initial' && (
<TelegramLink errorMessage={error} onSubmit={handleTelegramCheck} bot={telegram.bot} token={telegram.token} />
)}
{!loading && step === 'subscribed' && (
<>
<p>{intl.formatMessage(messages.haveSubscribed)}</p>
<Button kind="primary" size="large" onClick={handleTelegramUnsubscribe}>
{intl.formatMessage(messages.unsubscribe)}
</Button>
</>
)}
{!loading && step === 'unsubscribed' && (
<>
<p>{intl.formatMessage(messages.haveUnsubscribed)}</p>
<Button kind="primary" size="large" onClick={handleTelegramResubscribe}>
{intl.formatMessage(messages.resubscribe)}
</Button>
</>
)}
</div>
);
};
export const SubscribeByTelegram: FunctionComponent = () => {
const theme = useTheme();
const intl = useIntl();
const user = useSelector<StoreState, User | null>(({ user }) => user);
const isAnonymous = isUserAnonymous(user);
const buttonTitle = intl.formatMessage(isAnonymous ? messages.onlyRegisteredUsers : messages.subscribeByTelegram);
return (
<Dropdown
title={intl.formatMessage(messages.telegram)}
theme={theme}
disabled={isAnonymous}
buttonTitle={buttonTitle}
>
<SubscribeByTelegramForm />
</Dropdown>
);
};
@@ -0,0 +1 @@
export { SubscribeByTelegram } from './comment-form__subscribe-by-telegram';
@@ -122,10 +122,42 @@ describe('<CommentForm />', () => {
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();
});
});
@@ -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<Props, State> {
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<Props, State> {
</>
)}
{isEmailSubscription && <SubscribeByEmail />}
{(isRssSubscription && isTelegramSubscription) || (isEmailSubscription && isTelegramSubscription) ? (
<>
{' '}
<FormattedMessage id="commentForm.subscribe-or" defaultMessage="or" />{' '}
</>
) : null}
{isTelegramSubscription && <SubscribeByTelegram />}
</>
);
};
@@ -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);
}
@@ -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%;
}
@@ -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('<TelegramLink/>', () => {
let widthBackup: number;
beforeAll(() => {
widthBackup = window.screen.width;
});
afterAll(() => {
Object.defineProperties(window.screen, {
width: {
writable: true,
value: widthBackup!,
},
});
});
it('should render text', () => {
render(<TelegramLink {...defaultProps} />);
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(<TelegramLink {...defaultProps} />);
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(<TelegramLink {...defaultProps} />);
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(<TelegramLink {...defaultProps} />);
expect(screen.queryByAltText(/Telegram QR-code/)).not.toBeInTheDocument();
});
it('should show error if any', () => {
render(<TelegramLink {...defaultProps} errorMessage="Foo Error" />);
expect(screen.getByText('Foo Error')).toBeInTheDocument();
});
});
@@ -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<TelegramLinkProps> = ({ bot, token, errorMessage, onSubmit }) => {
const intl = useIntl();
const telegramLink = `https://t.me/${bot}/?start=${token}`;
return (
<>
<p className={clsx('telegram', styles.telegram)}>
{intl.formatMessage(messages.telegramMessage1)}{' '}
<a target="_blank" rel="noopener noreferrer" href={telegramLink}>
{intl.formatMessage(messages.telegramLink)}
</a>
{window.screen.width >= 768 && ` ${intl.formatMessage(messages.telegramOptionalQR)}`}{' '}
{intl.formatMessage(messages.telegramMessage2)}
<br />
{intl.formatMessage(messages.telegramMessage3)}
</p>
{window.screen.width >= 768 && (
<img
src={`${BASE_URL}${API_BASE}/qr/telegram?url=${telegramLink}`}
height="200"
width="200"
className={clsx('telegram-qr', styles.telegramQR)}
alt={intl.formatMessage(messages.telegramQR)}
/>
)}
{errorMessage && <div className={clsx('auth-error', styles.error)}>{errorMessage}</div>}
<Button
key="submit"
kind="primary"
size="large"
className={clsx('auth-submit', styles.button)}
type="submit"
onClick={onSubmit}
>
{intl.formatMessage(messages.telegramCheck)}
</Button>
</>
);
};
@@ -0,0 +1,32 @@
import { defineMessages } from 'react-intl';
export const messages = defineMessages<string>({
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',
},
});
@@ -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);
});
});
@@ -0,0 +1,23 @@
import { useState, StateUpdater } from 'preact/hooks';
function useSessionStorage<T>(key: string, initialValue?: T): [T, StateUpdater<T>] {
const [storedValue, setStoredValue] = useState<T>(() => {
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 };
@@ -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": "الرمز غير صالح",
@@ -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": "Токен несапраўдны",
@@ -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": "Токенът е невалиден",
@@ -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",
@@ -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ý",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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": "トークンが無効です",
@@ -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": "토큰이 유효하지 않습니다",
@@ -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.",
@@ -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": "Токен недействителен",
@@ -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": "โทเค็นไม่ถูกต้อง",
@@ -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",
@@ -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": "Токен недійсний",
@@ -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ệ",
@@ -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",
@@ -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": "令牌无效",
@@ -43,6 +43,7 @@
--color38: #ef0000;
--color27: #f98989;
--color26: #ffd7d7;
--color39: #6d4b8d;
--color30: 204, 6, 6;
--color12: 37, 158, 6;
+4
View File
@@ -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.
+1
View File
@@ -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",
+12
View File
@@ -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'}