show subscription buttons in simple view, add ability to hide rss button
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
jest.mock('common/settings', () => ({
|
||||
siteId: 'remark',
|
||||
}));
|
||||
@@ -1,19 +0,0 @@
|
||||
const settingsMock: typeof import('common/settings') = {
|
||||
...jest.requireActual('common/settings'),
|
||||
siteId: 'remark',
|
||||
pageTitle: 'remark test',
|
||||
url: 'https://remark42.com/test',
|
||||
maxShownComments: 20,
|
||||
token: 'abcd',
|
||||
theme: 'light',
|
||||
querySettings: {
|
||||
site_id: 'remark',
|
||||
page_title: 'remark test',
|
||||
url: 'https://remark42.com/test',
|
||||
max_shown_comments: 20,
|
||||
token: 'abcd',
|
||||
theme: 'light',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = settingsMock;
|
||||
@@ -1,38 +1,28 @@
|
||||
import { parseQuery } from 'utils/parse-query';
|
||||
|
||||
import type { Theme } from './types';
|
||||
import { THEMES, MAX_SHOWN_ROOT_COMMENTS } from './constants';
|
||||
|
||||
export interface QuerySettingsType {
|
||||
site_id?: string;
|
||||
page_title?: string;
|
||||
url?: string;
|
||||
max_shown_comments?: number;
|
||||
theme: Theme;
|
||||
/* used in delete users data page */
|
||||
token?: string;
|
||||
show_email_subscription?: boolean;
|
||||
function parseNumber(value: unknown) {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = +value;
|
||||
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
export const querySettings: Partial<QuerySettingsType> = parseQuery();
|
||||
|
||||
if (querySettings.max_shown_comments) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
querySettings.max_shown_comments = parseInt(querySettings.max_shown_comments as any as string, 10);
|
||||
} else {
|
||||
querySettings.max_shown_comments = MAX_SHOWN_ROOT_COMMENTS;
|
||||
function includes<T extends U, U>(coll: ReadonlyArray<T>, el: U): el is T {
|
||||
return coll.includes(el as T);
|
||||
}
|
||||
|
||||
if (!querySettings.theme || THEMES.indexOf(querySettings.theme) === -1) {
|
||||
querySettings.theme = THEMES[0];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
querySettings.show_email_subscription = (querySettings.show_email_subscription as any) !== 'false';
|
||||
|
||||
export const siteId = querySettings.site_id!;
|
||||
export const pageTitle = querySettings.page_title;
|
||||
export const url = querySettings.url;
|
||||
export const maxShownComments = querySettings.max_shown_comments;
|
||||
export const token = querySettings.token!;
|
||||
export const theme = querySettings.theme;
|
||||
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 isRssSubscription =
|
||||
rawParams.show_rss_subscription === undefined || rawParams.show_rss_subscription !== 'false';
|
||||
export const theme = (rawParams.theme = includes(THEMES, rawParams.theme) ? rawParams.theme : THEMES[0]);
|
||||
export const siteId = rawParams.site_id || 'remark';
|
||||
export const pageTitle = rawParams.page_title;
|
||||
export const url = rawParams.url;
|
||||
export const token = rawParams.token;
|
||||
export const locale = rawParams.locale || 'en';
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Config } from './types';
|
||||
import { QuerySettingsType, querySettings } from './settings';
|
||||
|
||||
interface StaticStoreType {
|
||||
config: Config;
|
||||
query: QuerySettingsType;
|
||||
/** used in fetcher, fer example to set comment edit timeout */
|
||||
serverClientTimeDiff?: number;
|
||||
}
|
||||
@@ -32,5 +30,4 @@ export const StaticStore: StaticStoreType = {
|
||||
telegram_bot_username: '',
|
||||
emoji_enabled: false,
|
||||
},
|
||||
query: querySettings as QuerySettingsType,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/preact';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { render } from 'tests/utils';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
import { LS_SAVED_COMMENT_VALUE } from 'common/constants';
|
||||
import * as localStorageModule from 'common/local-storage';
|
||||
|
||||
import { CommentForm, CommentFormProps, messages } from './comment-form';
|
||||
|
||||
const user: CommentFormProps['user'] = {
|
||||
name: 'username',
|
||||
id: 'id_1',
|
||||
picture: '',
|
||||
ip: '',
|
||||
admin: false,
|
||||
block: false,
|
||||
verified: false,
|
||||
};
|
||||
|
||||
function setup(
|
||||
overrideProps: Partial<CommentFormProps> = {},
|
||||
overrideConfig: Partial<typeof StaticStore['config']> = {}
|
||||
) {
|
||||
Object.assign(StaticStore.config, overrideConfig);
|
||||
|
||||
const props = {
|
||||
mode: 'main',
|
||||
theme: 'light',
|
||||
onSubmit: () => Promise.resolve(),
|
||||
getPreview: () => Promise.resolve(''),
|
||||
user: null,
|
||||
id: '1',
|
||||
...overrideProps,
|
||||
} as CommentFormProps;
|
||||
const CommentFormWithIntl = () => <CommentForm {...props} intl={useIntl()} />;
|
||||
|
||||
return render(<CommentFormWithIntl />);
|
||||
}
|
||||
describe('<CommentForm />', () => {
|
||||
afterEach(() => {
|
||||
// reset textarea id in order to have `textarea_1` for every test
|
||||
CommentForm.textareaId = 0;
|
||||
});
|
||||
|
||||
describe('with initial comment value', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
it('should has empty value', () => {
|
||||
const value = 'text';
|
||||
|
||||
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: value }));
|
||||
setup();
|
||||
expect(screen.getByTestId('textarea_1')).toHaveValue(value);
|
||||
});
|
||||
|
||||
it('should get initial value from localStorage', () => {
|
||||
const value = 'text';
|
||||
|
||||
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: value }));
|
||||
setup();
|
||||
expect(screen.getByTestId('textarea_1')).toHaveValue(value);
|
||||
});
|
||||
it('should get initial value from props instead localStorage', () => {
|
||||
const value = 'text from props';
|
||||
|
||||
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: 'text from localStorage' }));
|
||||
|
||||
setup({ value });
|
||||
expect(screen.getByTestId('textarea_1')).toHaveValue(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update initial value', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
it('should update value', () => {
|
||||
setup();
|
||||
|
||||
fireEvent.input(screen.getByTestId('textarea_1'), { target: { value: '1' } });
|
||||
expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{"1":"1"}');
|
||||
|
||||
fireEvent.input(screen.getByTestId('textarea_1'), { target: { value: '11' } });
|
||||
expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{"1":"11"}');
|
||||
});
|
||||
|
||||
it('should clear value after send', async () => {
|
||||
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: 'asd' }));
|
||||
const updateJsonItemSpy = jest.spyOn(localStorageModule, 'updateJsonItem');
|
||||
|
||||
setup();
|
||||
fireEvent.submit(screen.getByTestId('textarea_1'));
|
||||
await waitFor(() => {
|
||||
expect(updateJsonItemSpy).toHaveBeenCalled();
|
||||
});
|
||||
expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{}');
|
||||
});
|
||||
});
|
||||
|
||||
it(`doesn't render preview button and markdown toolbar in simple mode`, () => {
|
||||
setup({ user }, { simple_view: true });
|
||||
expect(screen.queryByTestId('markdown-toolbar')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Preview')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each`
|
||||
expected | value
|
||||
${'99'} | ${'That was Wintermute, manipulating the lock the way it had manipulated the drone micro and the chassis of a gutted game console. It was chambered for .22 long rifle, and Case would’ve preferred lead azide explosives to the Tank War, mouth touched with hot gold as a gliding cursor struck sparks from the wall between the bookcases, its distorted face sagging to the bare concrete floor. Splayed in his elastic g-web, Case watched the other passengers as he made his way down Shiga from the sushi stall he cradled it in his jacket pocket. Images formed and reformed: a flickering montage of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the Japanese night like live wire voodoo and he’d cry for it, cry in his jacket pocket. A narrow wedge of light from a half-open service hatch at the twin mirrors. Still it was a square of faint light. The alarm still oscillated, louder here, the rear wall dulling the roar of the arcade showed him broken lengths of damp chipboard and the robot gardener. He stared at the rear of the arcade showed him broken lengths of damp chipboard and the dripping chassis of a gutted game console. That was Wintermute, manipulating the lock the way it had manipulated the drone micro and the chassis of a gutted game console. It was chambered for .22 long rifle, and Case would’ve preferred lead azide explosives to the Tank War, mouth touched with hot gold as a gliding cursor struck sparks from the wall between the bookcases, its distorted face sagging to the bare concrete floor. Splayed in his elastic g-web, Case watched the other passengers as he made his way down Shiga from the sushi stall he cradled it in his jacket pocket. Images formed and reformed: a flickering montage of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the Japanese night like live wire voodoo and he’d cry for it, cry in his jacket.'}
|
||||
${'0'} | ${'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus. Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi consectetuer lacinia. Nam pretium turpis et arcu. Duis arcu tortor, suscipit eget, imperdiet nec, imperdiet iaculis, ipsum. Sed aliquam ultrices mauris. Integer ante arcu, accumsan a, consectetuer eget, posuere ut, mauris. Praesent adipiscing. Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestib'}
|
||||
${'-425'} | ${'All the speed he took, all the turns he’d taken and the amplified breathing of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the dark. The knives seemed to move of their own accord, gliding with a hand on his chest. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the Flatline as a construct, a hardwired ROM cassette replicating a dead man’s skills, obsessions, kneejerk responses. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the bright void beyond the chain link. Now this quiet courtyard, Sunday afternoon, this girl with a random collection of European furniture, as though Deane had once intended to use the place as his home. Now this quiet courtyard, Sunday afternoon, this girl with a ritual lack of urgency through the arcs and passes of their dance, point passing point, as the men waited for an opening. They floated in the shade beneath a bridge or overpass. A graphic representation of data abstracted from the banks of every computer in the coffin for Armitage’s call. All the speed he took, all the turns he’d taken and the amplified breathing of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the dark. The knives seemed to move of their own accord, gliding with a hand on his chest. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the Flatline as a construct, a hardwired ROM cassette replicating a dead man’s skills, obsessions, kneejerk responses. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the bright void beyond the chain link. Now this quiet courtyard, Sunday afternoon, this girl with a random collection of European furniture, as though Deane had once intended to use the place as his home. Now this quiet courtyard, Sunday afternoon, this girl with a ritual lack of urgency through the arcs and passes of their dance, point passing point, as the men waited for an opening. They floated in the shade beneath a bridge or overpass. A graphic representation of data abstracted from the banks of every computer in the coffin for Armitage’s call.'}
|
||||
`('renders counter of rest symbols', async ({ value, expected }) => {
|
||||
setup({ value }, { max_comment_size: 2000 });
|
||||
expect(screen.getByText(expected)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when authorized', () => {
|
||||
describe('with simple view', () => {
|
||||
it('renders email subscription button', () => {
|
||||
setup({ user }, { simple_view: true, email_notifications: true });
|
||||
expect(screen.getByText(/Subscribe by/)).toBeVisible();
|
||||
expect(screen.getByTitle('Subscribe by Email')).toBeVisible();
|
||||
});
|
||||
it('renders rss subscription button', () => {
|
||||
setup({ user }, { simple_view: true });
|
||||
expect(screen.getByText(/Subscribe by/)).toBeVisible();
|
||||
expect(screen.getByTitle('Subscribe by RSS')).toBeVisible();
|
||||
});
|
||||
});
|
||||
it('renders without email subscription button when email_notifications disabled', () => {
|
||||
setup({ user }, { email_notifications: false });
|
||||
expect(screen.queryByText('Subscribe by RSS')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when unauthorized', () => {
|
||||
it(`doesn't email subscription button`, () => {
|
||||
setup();
|
||||
expect(screen.queryByText(/Subscribe by/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTitle('Subscribe bey Email')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it(`doesn't render rss subscription button`, () => {
|
||||
setup();
|
||||
expect(screen.queryByText(/Subscribe by/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Subscribe by RSS')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show error message of image upload try by anonymous user', () => {
|
||||
setup({ user: { ...user, id: 'anonymous_1' } });
|
||||
fireEvent.drop(screen.getByTestId('commentform_1'));
|
||||
expect(screen.getByText(messages.anonymousUploadingDisabled.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,195 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { user, anonymousUser } from '__stubs__/user';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
import { LS_SAVED_COMMENT_VALUE } from 'common/constants';
|
||||
import * as localStorageModule from 'common/local-storage';
|
||||
import { TextareaAutosize } from 'components/textarea-autosize';
|
||||
|
||||
import { CommentForm, CommentFormProps, messages } from './comment-form';
|
||||
import { SubscribeByEmail } from './__subscribe-by-email';
|
||||
import { IntlShape } from 'react-intl';
|
||||
|
||||
function createEvent<E extends Event, T = unknown>(type: string, value: T): E {
|
||||
const event = new Event(type);
|
||||
|
||||
Object.defineProperty(event, 'target', { value });
|
||||
|
||||
return event as E;
|
||||
}
|
||||
|
||||
const DEFAULT_PROPS: Readonly<Omit<CommentFormProps, 'intl'>> = {
|
||||
mode: 'main',
|
||||
theme: 'light',
|
||||
onSubmit: () => Promise.resolve(),
|
||||
getPreview: () => Promise.resolve(''),
|
||||
user: null,
|
||||
id: '1',
|
||||
};
|
||||
|
||||
const intl = {
|
||||
formatMessage(message: { defaultMessage: string }) {
|
||||
return message.defaultMessage || '';
|
||||
},
|
||||
} as IntlShape;
|
||||
|
||||
describe('<CommentForm />', () => {
|
||||
it('should shallow without control panel, preview button, and rss links in "simple view" mode', () => {
|
||||
const props = { ...DEFAULT_PROPS, simpleView: true, intl };
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
|
||||
expect(wrapper.exists('.comment-form__control-panel')).toEqual(false);
|
||||
expect(wrapper.exists('.comment-form__button_type_preview')).toEqual(false);
|
||||
expect(wrapper.exists('.comment-form__rss')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should be shallowed with email subscription button', () => {
|
||||
StaticStore.config.email_notifications = true;
|
||||
|
||||
const props = { ...DEFAULT_PROPS, user, intl };
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
|
||||
expect(wrapper.exists(SubscribeByEmail)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be rendered without email subscription button when email_notifications disabled', () => {
|
||||
StaticStore.config.email_notifications = false;
|
||||
|
||||
const props = { ...DEFAULT_PROPS, user, intl };
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
|
||||
expect(wrapper.exists(SubscribeByEmail)).toEqual(false);
|
||||
});
|
||||
|
||||
describe('initial value of comment', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
it('should has empty value', () => {
|
||||
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 2: 'text' }));
|
||||
|
||||
const props = { ...DEFAULT_PROPS, user, intl };
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
|
||||
expect(wrapper.state('text')).toBe('');
|
||||
expect(wrapper.find(TextareaAutosize).prop('value')).toBe('');
|
||||
});
|
||||
|
||||
it('should get initial value from localStorage', () => {
|
||||
const COMMENT_VALUE = 'text';
|
||||
|
||||
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: COMMENT_VALUE }));
|
||||
|
||||
const props = { ...DEFAULT_PROPS, user, intl };
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
|
||||
expect(wrapper.state('text')).toBe(COMMENT_VALUE);
|
||||
expect(wrapper.find(TextareaAutosize).prop('value')).toBe(COMMENT_VALUE);
|
||||
});
|
||||
|
||||
it('should get initial value from props instead localStorage', () => {
|
||||
const COMMENT_VALUE = 'text from props';
|
||||
|
||||
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: 'text from localStorage' }));
|
||||
|
||||
const props = { ...DEFAULT_PROPS, user, intl, value: COMMENT_VALUE };
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
|
||||
expect(wrapper.state('text')).toBe(COMMENT_VALUE);
|
||||
expect(wrapper.find(TextareaAutosize).prop('value')).toBe(COMMENT_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update value of comment in localStorage', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
it('should update value', () => {
|
||||
const props = { ...DEFAULT_PROPS, user, intl };
|
||||
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
instance.onInput(createEvent('input', { value: '1' }));
|
||||
expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{"1":"1"}');
|
||||
|
||||
instance.onInput(createEvent('input', { value: '11' }));
|
||||
expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{"1":"11"}');
|
||||
});
|
||||
|
||||
it('should clear value after send', async () => {
|
||||
localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ '1': 'asd' }));
|
||||
const updateJsonItemSpy = jest.spyOn(localStorageModule, 'updateJsonItem');
|
||||
const props = { ...DEFAULT_PROPS, user, intl };
|
||||
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
await instance.send(createEvent('send', { preventDefault: () => undefined }));
|
||||
expect(updateJsonItemSpy).toHaveBeenCalled();
|
||||
expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe(JSON.stringify({}));
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error message of image upload try by anonymous user', () => {
|
||||
const props = { ...DEFAULT_PROPS, user: anonymousUser, intl };
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
instance.onDrop(new Event('drag') as DragEvent);
|
||||
expect(wrapper.exists('.comment-form__error')).toEqual(true);
|
||||
expect(wrapper.find('.comment-form__error').text()).toEqual(messages.anonymousUploadingDisabled.defaultMessage);
|
||||
});
|
||||
|
||||
it('should show error message of image upload try by unauthorized user', () => {
|
||||
const props = { ...DEFAULT_PROPS, intl };
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
instance.onDrop(new Event('drag') as DragEvent);
|
||||
expect(wrapper.exists('.comment-form__error')).toEqual(true);
|
||||
expect(wrapper.find('.comment-form__error').text()).toEqual(messages.unauthorizedUploadingDisabled.defaultMessage);
|
||||
});
|
||||
|
||||
it('should show rest letters counter', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const originalConfig = { ...StaticStore.config };
|
||||
StaticStore.config.max_comment_size = 2000;
|
||||
const props = { ...DEFAULT_PROPS, intl };
|
||||
const wrapper = shallow<CommentForm>(<CommentForm {...props} />);
|
||||
const instance = wrapper.instance();
|
||||
const text =
|
||||
'That was Wintermute, manipulating the lock the way it had manipulated the drone micro and the chassis of a gutted game console. It was chambered for .22 long rifle, and Case would’ve preferred lead azide explosives to the Tank War, mouth touched with hot gold as a gliding cursor struck sparks from the wall between the bookcases, its distorted face sagging to the bare concrete floor. Splayed in his elastic g-web, Case watched the other passengers as he made his way down Shiga from the sushi stall he cradled it in his jacket pocket. Images formed and reformed: a flickering montage of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the Japanese night like live wire voodoo and he’d cry for it, cry in his jacket pocket. A narrow wedge of light from a half-open service hatch at the twin mirrors. Still it was a square of faint light. The alarm still oscillated, louder here, the rear wall dulling the roar of the arcade showed him broken lengths of damp chipboard and the robot gardener. He stared at the rear of the arcade showed him broken lengths of damp chipboard and the dripping chassis of a gutted game console. That was Wintermute, manipulating the lock the way it had manipulated the drone micro and the chassis of a gutted game console. It was chambered for .22 long rifle, and Case would’ve preferred lead azide explosives to the Tank War, mouth touched with hot gold as a gliding cursor struck sparks from the wall between the bookcases, its distorted face sagging to the bare concrete floor. Splayed in his elastic g-web, Case watched the other passengers as he made his way down Shiga from the sushi stall he cradled it in his jacket pocket. Images formed and reformed: a flickering montage of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the Japanese night like live wire voodoo and he’d cry for it, cry in his jacket.';
|
||||
|
||||
instance.setState({ text });
|
||||
await wrapper.update();
|
||||
|
||||
expect(instance.state.text).toBe(text);
|
||||
expect(wrapper.find('.comment-form__counter').exists()).toBe(true);
|
||||
expect(wrapper.find('.comment-form__counter').text()).toBe('99');
|
||||
|
||||
StaticStore.config = originalConfig;
|
||||
});
|
||||
|
||||
it('should show zero in rest letters counter', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const originalConfig = { ...StaticStore.config };
|
||||
StaticStore.config.max_comment_size = 2000;
|
||||
const props = { ...DEFAULT_PROPS, intl };
|
||||
const wrapper = shallow<CommentForm, CommentFormProps>(<CommentForm {...props} />);
|
||||
const instance = wrapper.instance();
|
||||
const text =
|
||||
'All the speed he took, all the turns he’d taken and the amplified breathing of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the dark. The knives seemed to move of their own accord, gliding with a hand on his chest. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the Flatline as a construct, a hardwired ROM cassette replicating a dead man’s skills, obsessions, kneejerk responses. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the bright void beyond the chain link. Now this quiet courtyard, Sunday afternoon, this girl with a random collection of European furniture, as though Deane had once intended to use the place as his home. Now this quiet courtyard, Sunday afternoon, this girl with a ritual lack of urgency through the arcs and passes of their dance, point passing point, as the men waited for an opening. They floated in the shade beneath a bridge or overpass. A graphic representation of data abstracted from the banks of every computer in the coffin for Armitage’s call. All the speed he took, all the turns he’d taken and the amplified breathing of the Sprawl’s towers and ragged Fuller domes, dim figures moving toward him in the dark. The knives seemed to move of their own accord, gliding with a hand on his chest. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the Flatline as a construct, a hardwired ROM cassette replicating a dead man’s skills, obsessions, kneejerk responses. Case had never seen him wear the same suit twice, although his wardrobe seemed to consist entirely of meticulous reconstruction’s of garments of the bright void beyond the chain link. Now this quiet courtyard, Sunday afternoon, this girl with a random collection of European furniture, as though Deane had once intended to use the place as his home. Now this quiet courtyard, Sunday afternoon, this girl with a ritual lack of urgency through the arcs and passes of their dance, point passing point, as the men waited for an opening. They floated in the shade beneath a bridge or overpass. A graphic representation of data abstracted from the banks of every computer in the coffin for Armitage’s call.';
|
||||
|
||||
instance.onInput(createEvent('input', { value: text }));
|
||||
|
||||
await wrapper.update();
|
||||
|
||||
expect(instance.state.text).toBe(text.substr(0, StaticStore.config.max_comment_size));
|
||||
expect(wrapper.find('.comment-form__counter').text()).toBe('0');
|
||||
|
||||
StaticStore.config = originalConfig;
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import b, { Mix } from 'bem-react-helper';
|
||||
|
||||
import { User, Theme, Image, ApiError } from 'common/types';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
import { pageTitle } from 'common/settings';
|
||||
import * as settings from 'common/settings';
|
||||
import { extractErrorMessageFromResponse } from 'utils/errorUtils';
|
||||
import { isUserAnonymous } from 'utils/isUserAnonymous';
|
||||
import { sleep } from 'utils/sleep';
|
||||
@@ -21,8 +21,6 @@ import { SubscribeByRSS } from './__subscribe-by-rss';
|
||||
import { MarkdownToolbar } from './markdown-toolbar';
|
||||
import { TextExpander } from './text-expander';
|
||||
|
||||
let textareaId = 0;
|
||||
|
||||
export type CommentFormProps = {
|
||||
id: string;
|
||||
user: User | null;
|
||||
@@ -31,14 +29,13 @@ export type CommentFormProps = {
|
||||
mix?: Mix;
|
||||
mode?: 'main' | 'edit' | 'reply';
|
||||
theme: Theme;
|
||||
simpleView?: boolean;
|
||||
autofocus?: boolean;
|
||||
|
||||
onSubmit(text: string, pageTitle: string): Promise<void>;
|
||||
getPreview(text: string): Promise<string>;
|
||||
/** action on cancel. optional as root input has no cancel option */
|
||||
onCancel?: () => void;
|
||||
uploadImage?: (image: File) => Promise<Image>;
|
||||
onCancel?(): void;
|
||||
uploadImage?(image: File): Promise<Image>;
|
||||
intl: IntlShape;
|
||||
};
|
||||
|
||||
@@ -101,38 +98,24 @@ export const messages = defineMessages({
|
||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
/** reference to textarea element */
|
||||
textareaRef = createRef<HTMLTextAreaElement>();
|
||||
textareaId: string;
|
||||
static textareaId = 0;
|
||||
|
||||
state = {
|
||||
preview: null,
|
||||
isErrorShown: false,
|
||||
errorMessage: null,
|
||||
errorLock: false,
|
||||
isDisabled: false,
|
||||
text: '',
|
||||
buttonText: null,
|
||||
};
|
||||
|
||||
constructor(props: CommentFormProps) {
|
||||
super(props);
|
||||
textareaId = textareaId + 1;
|
||||
this.textareaId = `textarea_${textareaId}`;
|
||||
|
||||
const savedComments = getJsonItem<Record<string, string>>(LS_SAVED_COMMENT_VALUE);
|
||||
let text = savedComments?.[props.id] ?? '';
|
||||
|
||||
if (props.value) {
|
||||
text = props.value;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
preview: null,
|
||||
isErrorShown: false,
|
||||
errorMessage: null,
|
||||
errorLock: false,
|
||||
isDisabled: false,
|
||||
text,
|
||||
buttonText: null,
|
||||
};
|
||||
|
||||
this.getPreview = this.getPreview.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.onDragOver = this.onDragOver.bind(this);
|
||||
this.onDrop = this.onDrop.bind(this);
|
||||
this.appendError = this.appendError.bind(this);
|
||||
this.uploadImage = this.uploadImage.bind(this);
|
||||
this.uploadImages = this.uploadImages.bind(this);
|
||||
this.onPaste = this.onPaste.bind(this);
|
||||
this.state.text = props.value ?? savedComments?.[props.id] ?? '';
|
||||
CommentForm.textareaId += 1;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: CommentFormProps) {
|
||||
@@ -161,12 +144,12 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
);
|
||||
}
|
||||
|
||||
onKeyDown(e: KeyboardEvent) {
|
||||
onKeyDown = (e: KeyboardEvent) => {
|
||||
// send on cmd+enter / ctrl+enter
|
||||
if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
|
||||
this.send(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onInput = (e: Event) => {
|
||||
const { value } = e.target as HTMLInputElement;
|
||||
@@ -190,14 +173,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
});
|
||||
};
|
||||
|
||||
async onPaste(e: ClipboardEvent) {
|
||||
onPaste = async (e: ClipboardEvent) => {
|
||||
if (!(e.clipboardData && e.clipboardData.files.length > 0)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const files = Array.from(e.clipboardData.files);
|
||||
await this.uploadImages(files);
|
||||
}
|
||||
};
|
||||
|
||||
send = async (e: Event) => {
|
||||
const { text } = this.state;
|
||||
@@ -212,7 +195,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
|
||||
this.setState({ isDisabled: true, isErrorShown: false, text });
|
||||
try {
|
||||
await this.props.onSubmit(text, pageTitle || document.title);
|
||||
await this.props.onSubmit(text, settings.pageTitle || document.title);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
isDisabled: false,
|
||||
@@ -233,7 +216,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
this.setState({ isDisabled: false, preview: null, text: '' });
|
||||
};
|
||||
|
||||
getPreview() {
|
||||
getPreview = () => {
|
||||
const text = this.textareaRef.current?.value ?? this.state.text;
|
||||
|
||||
if (!text || !text.trim()) return;
|
||||
@@ -246,10 +229,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
.catch(() => {
|
||||
this.setState({ isErrorShown: true, errorMessage: null });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** appends error to input's error block */
|
||||
appendError(...errors: string[]) {
|
||||
appendError = (...errors: string[]) => {
|
||||
if (!this.state.errorMessage) {
|
||||
this.setState({
|
||||
errorMessage: errors.join('\n'),
|
||||
@@ -261,9 +244,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
errorMessage: `${this.state.errorMessage}\n${errors.join('\n')}`,
|
||||
isErrorShown: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDragOver(e: DragEvent) {
|
||||
onDragOver = (e: DragEvent) => {
|
||||
if (!this.props.user) e.preventDefault();
|
||||
if (!this.props.uploadImage) return;
|
||||
if (StaticStore.config.max_image_size === 0) return;
|
||||
@@ -273,9 +256,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
if (Array.from(items).filter((i) => i.kind === 'file' && ImageMimeRegex.test(i.type)).length === 0) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
};
|
||||
|
||||
onDrop(e: DragEvent) {
|
||||
onDrop = (e: DragEvent) => {
|
||||
const isAnonymous = this.props.user && isUserAnonymous(this.props.user);
|
||||
if (!this.props.user || isAnonymous) {
|
||||
const message = isAnonymous ? messages.anonymousUploadingDisabled : messages.unauthorizedUploadingDisabled;
|
||||
@@ -296,7 +279,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
e.preventDefault();
|
||||
|
||||
this.uploadImages(data);
|
||||
}
|
||||
};
|
||||
|
||||
/** returns selection range of a textarea */
|
||||
getSelection(): [number, number] {
|
||||
@@ -323,7 +306,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
}
|
||||
|
||||
/** wrapper with error handling for props.uploadImage */
|
||||
uploadImage(file: File): Promise<Image | Error> {
|
||||
uploadImage = (file: File): Promise<Image | Error> => {
|
||||
const intl = this.props.intl;
|
||||
return this.props.uploadImage!(file).catch((e: ApiError | string) => {
|
||||
return new Error(
|
||||
@@ -333,10 +316,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** performs upload process */
|
||||
async uploadImages(files: File[]) {
|
||||
uploadImages = async (files: File[]) => {
|
||||
const intl = this.props.intl;
|
||||
if (!this.props.uploadImage) return;
|
||||
if (!this.textareaRef.current) return;
|
||||
@@ -419,7 +402,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
}
|
||||
|
||||
this.setState({ errorLock: false, isDisabled: false, buttonText: null });
|
||||
}
|
||||
};
|
||||
|
||||
renderMarkdownTip = () => (
|
||||
<div className="comment-form__markdown">
|
||||
@@ -437,8 +420,32 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
</div>
|
||||
);
|
||||
|
||||
renderSubscribeButtons = () => {
|
||||
const isEmailNotifications = StaticStore.config.email_notifications;
|
||||
const isEmailSubscription = isEmailNotifications && settings.isEmailSubscription;
|
||||
const { isRssSubscription } = settings;
|
||||
|
||||
if (!isRssSubscription && !isEmailSubscription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormattedMessage id="commentForm.subscribe-by" defaultMessage="Subscribe by" />{' '}
|
||||
{isRssSubscription && <SubscribeByRSS userId={this.props.user?.id ?? null} />}
|
||||
{isRssSubscription && isEmailSubscription && (
|
||||
<>
|
||||
{' '}
|
||||
<FormattedMessage id="commentForm.subscribe-or" defaultMessage="or" />{' '}
|
||||
</>
|
||||
)}
|
||||
{isEmailSubscription && <SubscribeByEmail />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { theme, mode, simpleView, mix, uploadImage, autofocus, user, intl } = this.props;
|
||||
const { theme, mode, mix, uploadImage, autofocus, user, intl } = this.props;
|
||||
const { isDisabled, isErrorShown, preview, text, buttonText } = this.state;
|
||||
const charactersLeft = StaticStore.config.max_comment_size - text.length;
|
||||
const errorMessage = this.props.errorMessage || this.state.errorMessage;
|
||||
@@ -447,15 +454,18 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
edit: <FormattedMessage id="commentForm.save" defaultMessage="Save" />,
|
||||
reply: <FormattedMessage id="commentForm.reply" defaultMessage="Reply" />,
|
||||
};
|
||||
const textareaId = `textarea_${CommentForm.textareaId}`;
|
||||
const label = buttonText || Labels[mode || 'main'];
|
||||
const placeholderMessage = intl.formatMessage(messages.placeholder);
|
||||
const isSimpleView = StaticStore.config.simple_view;
|
||||
|
||||
return (
|
||||
<form
|
||||
className={b('comment-form', {
|
||||
mods: {
|
||||
theme,
|
||||
type: mode || 'reply',
|
||||
simple: simpleView,
|
||||
simple: isSimpleView,
|
||||
},
|
||||
mix,
|
||||
})}
|
||||
@@ -463,21 +473,22 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
aria-label={intl.formatMessage(messages.newComment)}
|
||||
onDragOver={this.onDragOver}
|
||||
onDrop={this.onDrop}
|
||||
data-testid={`commentform_${this.props.id}`}
|
||||
>
|
||||
{!simpleView && (
|
||||
<div className="comment-form__control-panel">
|
||||
{!isSimpleView && (
|
||||
<div className="comment-form__control-panel" data-testid="markdown-toolbar">
|
||||
<MarkdownToolbar
|
||||
intl={intl}
|
||||
allowUpload={Boolean(uploadImage)}
|
||||
uploadImages={this.uploadImages}
|
||||
textareaId={this.textareaId}
|
||||
textareaId={textareaId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="comment-form__field-wrapper">
|
||||
<TextExpander>
|
||||
<TextareaAutosize
|
||||
id={this.textareaId}
|
||||
id={textareaId}
|
||||
ref={this.textareaRef}
|
||||
onPaste={this.onPaste}
|
||||
className="comment-form__field"
|
||||
@@ -504,7 +515,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
{user ? (
|
||||
<>
|
||||
<div>
|
||||
{!simpleView && (
|
||||
{!isSimpleView && (
|
||||
<Button
|
||||
kind="secondary"
|
||||
theme={theme}
|
||||
@@ -521,17 +532,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!simpleView && mode === 'main' && (
|
||||
{mode === 'main' && (
|
||||
<div className="comment-form__rss">
|
||||
{this.renderMarkdownTip()}
|
||||
<FormattedMessage id="commentForm.subscribe-by" defaultMessage="Subscribe by" />{' '}
|
||||
<SubscribeByRSS userId={user !== null ? user.id : null} />
|
||||
{StaticStore.config.email_notifications && StaticStore.query.show_email_subscription && (
|
||||
<>
|
||||
{' '}
|
||||
<FormattedMessage id="commentForm.subscribe-or" defaultMessage="or" /> <SubscribeByEmail />
|
||||
</>
|
||||
)}
|
||||
{this.renderSubscribeButtons()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -525,7 +525,6 @@ export class Comment extends Component<CommentProps, State> {
|
||||
getPreview={this.props.getPreview!}
|
||||
autofocus={true}
|
||||
uploadImage={uploadImageHandler}
|
||||
simpleView={StaticStore.config.simple_view}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -544,7 +543,6 @@ export class Comment extends Component<CommentProps, State> {
|
||||
errorMessage={state.editDeadline === undefined ? intl.formatMessage(messages.expiredTime) : undefined}
|
||||
autofocus={true}
|
||||
uploadImage={uploadImageHandler}
|
||||
simpleView={StaticStore.config.simple_view}
|
||||
/>
|
||||
)}
|
||||
</article>
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { StoreState } from 'store';
|
||||
import { COMMENT_NODE_CLASSNAME_PREFIX, MAX_SHOWN_ROOT_COMMENTS, THEMES, IS_MOBILE } from 'common/constants';
|
||||
import { maxShownComments, url } from 'common/settings';
|
||||
|
||||
import { StaticStore } from 'common/static-store';
|
||||
import {
|
||||
setUser,
|
||||
fetchUser,
|
||||
@@ -250,7 +249,6 @@ export class Root extends Component<Props, State> {
|
||||
onSubmit={(text: string, title: string) => this.props.addComment(text, title)}
|
||||
getPreview={this.props.getPreview}
|
||||
uploadImage={imageUploadHandler}
|
||||
simpleView={StaticStore.config.simple_view}
|
||||
/>
|
||||
)}
|
||||
{this.props.pinnedComments.length > 0 && (
|
||||
|
||||
@@ -25,5 +25,5 @@ export const TextareaAutosize = forwardRef<HTMLTextAreaElement, Props>(({ onInpu
|
||||
autoResize(ref.current);
|
||||
}, [value, ref]);
|
||||
|
||||
return <textarea {...props} onInput={handleInput} value={value} ref={ref} />;
|
||||
return <textarea {...props} data-testid={props.id} onInput={handleInput} value={value} ref={ref} />;
|
||||
});
|
||||
|
||||
@@ -4,10 +4,7 @@ import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { loadLocale } from 'utils/loadLocale';
|
||||
import { getLocale } from 'utils/getLocale';
|
||||
import { parseQuery } from 'utils/parse-query';
|
||||
import { parseMessage } from 'utils/post-message';
|
||||
import { parseBooleansFromDictionary } from 'utils/parse-booleans-from-dictionary';
|
||||
import { ConnectedRoot } from 'components/root';
|
||||
import { Profile } from 'components/profile';
|
||||
import { store } from 'store';
|
||||
@@ -16,6 +13,7 @@ import { StaticStore } from 'common/static-store';
|
||||
import { getConfig } from 'common/api';
|
||||
import { fetchHiddenUsers } from 'store/user/actions';
|
||||
import { restoreCollapsedThreads } from 'store/thread/actions';
|
||||
import { locale, theme, rawParams } from 'common/settings';
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -32,8 +30,6 @@ async function init(): Promise<void> {
|
||||
throw new Error("Remark42: Can't find root node.");
|
||||
}
|
||||
|
||||
const params = parseQuery();
|
||||
const locale = getLocale(params);
|
||||
const messages = await loadLocale(locale).catch(() => ({}));
|
||||
const boundActions = bindActionCreators({ fetchHiddenUsers, restoreCollapsedThreads }, store.dispatch);
|
||||
|
||||
@@ -51,7 +47,7 @@ async function init(): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
if (params.theme === 'dark') {
|
||||
if (theme === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
}
|
||||
|
||||
@@ -59,12 +55,14 @@ async function init(): Promise<void> {
|
||||
boundActions.restoreCollapsedThreads();
|
||||
|
||||
const config = await getConfig();
|
||||
const optionsParams = parseBooleansFromDictionary(params, 'simple_view');
|
||||
StaticStore.config = { ...config, ...optionsParams };
|
||||
StaticStore.config = {
|
||||
...config,
|
||||
simple_view: rawParams.simple_view === undefined || rawParams.simple_view === 'true',
|
||||
};
|
||||
|
||||
render(
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<Provider store={store}>{params.page === 'profile' ? <Profile /> : <ConnectedRoot />}</Provider>
|
||||
<Provider store={store}>{rawParams.page === 'profile' ? <Profile /> : <ConnectedRoot />}</Provider>
|
||||
</IntlProvider>,
|
||||
node
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Theme } from 'common/types';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
import * as settings from 'common/settings';
|
||||
|
||||
import { THEME_SET_ACTION, THEME_SET } from './types';
|
||||
|
||||
export function theme(state: Theme = StaticStore.query.theme, action: THEME_SET_ACTION): Theme {
|
||||
export function theme(state: Theme = settings.theme, action: THEME_SET_ACTION): Theme {
|
||||
switch (action.type) {
|
||||
case THEME_SET: {
|
||||
return action.theme;
|
||||
|
||||
@@ -4,5 +4,5 @@ import { User } from 'common/types';
|
||||
* Defines whether current client is logged in via `Anonymous provider`
|
||||
*/
|
||||
export function isUserAnonymous(user: User | null) {
|
||||
return user === null || user.id.substr(0, 10) === 'anonymous_';
|
||||
return user === null || user?.id.substring(0, 10) === 'anonymous_';
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { parseBooleansFromDictionary } from './parse-booleans-from-dictionary';
|
||||
|
||||
const defaultProps = {
|
||||
components: 'embed,counter',
|
||||
host: 'http://127.0.0.1:9000',
|
||||
locale: 'ru',
|
||||
site_id: 'remark',
|
||||
theme: 'dark',
|
||||
};
|
||||
|
||||
describe('getConfigMerge', () => {
|
||||
it('when we need to get one field and it is "true"', () => {
|
||||
const params = {
|
||||
...defaultProps,
|
||||
simple: 'true',
|
||||
simple_view: 'true',
|
||||
};
|
||||
expect(parseBooleansFromDictionary(params, 'simple_view')).toEqual({ simple_view: true });
|
||||
});
|
||||
|
||||
it('when we need to get one field and it is "false"', () => {
|
||||
const params = {
|
||||
...defaultProps,
|
||||
simple: 'true',
|
||||
simple_view: 'false',
|
||||
};
|
||||
expect(parseBooleansFromDictionary(params, 'simple_view')).toEqual({ simple_view: false });
|
||||
});
|
||||
it('when we need to get one or more fields', () => {
|
||||
const params = {
|
||||
...defaultProps,
|
||||
simple: 'false',
|
||||
simple_view: 'true',
|
||||
};
|
||||
expect(parseBooleansFromDictionary(params, 'simple_view', 'simple')).toEqual({
|
||||
simple: false,
|
||||
simple_view: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('when the required field does not exist', () => {
|
||||
const params = {
|
||||
...defaultProps,
|
||||
simple: 'true',
|
||||
};
|
||||
expect(parseBooleansFromDictionary(params, 'simple_view')).toEqual({});
|
||||
});
|
||||
|
||||
it('when the field has the wrong format', () => {
|
||||
const params = {
|
||||
...defaultProps,
|
||||
simple: 'true',
|
||||
simple_view: 'dark',
|
||||
};
|
||||
expect(parseBooleansFromDictionary(params, 'simple_view', 'simple')).toEqual({ simple: true });
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
export function parseBooleansFromDictionary(input: Record<string, unknown>, ...args: string[]) {
|
||||
const result: Record<string, boolean> = {};
|
||||
for (let key of args) {
|
||||
if (input[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (input[key] === 'true') {
|
||||
result[key] = true;
|
||||
}
|
||||
if (input[key] === 'false') {
|
||||
result[key] = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
/** converts window.location.search into object */
|
||||
|
||||
export function parseQuery<T extends Record<string, string>>(search: string = window.location.search): T {
|
||||
const params: { [key: string]: string } = {};
|
||||
export function parseQuery(search: string = window.location.search): Record<string, string> {
|
||||
const params: Record<string, string> = {};
|
||||
new URLSearchParams(search).forEach((value: string, key: string) => {
|
||||
params[key] = value;
|
||||
});
|
||||
return params as T;
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ module.exports = {
|
||||
'<rootDir>/app/__mocks__/localstorage.ts',
|
||||
'<rootDir>/app/__stubs__/remark-config.ts',
|
||||
'<rootDir>/app/__stubs__/static-config.ts',
|
||||
'<rootDir>/app/__stubs__/settings.ts',
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'app/**/*.{ts,tsx}',
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
// },
|
||||
theme: theme,
|
||||
// locale: "ru",
|
||||
// simple_view: true,
|
||||
// simple_view: true
|
||||
};
|
||||
|
||||
(function (c, d) {
|
||||
|
||||
@@ -5,10 +5,9 @@ title: Frontend Configuration
|
||||
## Configuration
|
||||
|
||||
- **`host`**`: string` (required) – hostname of Remark42 server, same as REMARK_URL in backend config, e.g. "https://demo.remark42.com"
|
||||
- **`site_id`**`: string` (optional, `remark` by default) – the `SITE` that you passed to Remark42 instance on start of backend.
|
||||
- **`url`**`: string` (optional, `window.location.origin + window.location.pathname` by default) – url to the page with comments, it is used as unique identificator for comments thread
|
||||
- **`site_id`**`: string` (optional, `remark` by default) – the `SITE` that you passed to Remark42 instance on start of backend.
|
||||
- **`url`**`: string` (optional, `window.location.origin + window.location.pathname` by default) – url to the page with comments, it is used as unique identificator for comments thread
|
||||
Note that if you use query parameters as significant part of URL (the one that actually changes content on page) you will have to configure URL manually to keep query params, as `window.location.origin + window.location.pathname` doesn't contain query params and hash. For example, default URL for `https://example/com/example-post?id=1#hash` would be `https://example/com/example-post`
|
||||
|
||||
- **`components`**`: ['embed' | 'last-comments' | 'counter']` (optional, `['embed']` by default) – an array of widgets that should be rendered on a page. You may use more than one widget on a page.
|
||||
Available components are:
|
||||
- `'embed'` – basic comments widget
|
||||
@@ -19,9 +18,10 @@ title: Frontend Configuration
|
||||
- **`page_title`**`: string` (optional, `document.title` by default) – title for current comments page
|
||||
- **`locale`**`: enum` (optional, `'en'` by default) – interface localization, [check possible localizations](#locales)
|
||||
- **`show_email_subscription`**`: boolean` (optional, `true` by default) – enables email subscription feature in interface when enable it from backend side, 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_rss_subscription`**`: boolean` (optional, `true` by default) – enables RSS subscription feature in interface
|
||||
- **`simple_view`**`: boolean` (optional, `false` by default) – overrides the parameter from the backend minimized UI with basic info only
|
||||
|
||||
Example with all of the params:
|
||||
Example with all of the params:
|
||||
|
||||
```html
|
||||
<script>
|
||||
@@ -49,6 +49,7 @@ Add following **initialization** script after it.
|
||||
<script>!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src=remark_config.host+"/web/"+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);</script>
|
||||
```
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## Comments
|
||||
|
||||
It's the main widget that renders a list of comments with ability of commenting.
|
||||
@@ -57,19 +58,19 @@ Add following snippet in the place where you want to see Remark42 widget. The co
|
||||
```html
|
||||
<div id="remark42"></div>
|
||||
```
|
||||
|
||||
::: note 💡
|
||||
**Note:** The initialization script should be placed after the code mentioned above.
|
||||
:::
|
||||
|
||||
If you want to set this up on a Single Page App, see the [appropriate doc page](https://remark42.com/docs/configuration/frontend/spa/).
|
||||
|
||||
|
||||
#### Themes
|
||||
|
||||
Remark42 has two themes: light and dark. You can pick one using a configuration object, but there is also a possibility to switch between themes in runtime. For this purpose, Remark42 adds to the `window` object named `REMARK42`, which contains a function `changeTheme`. Just call this function and pass a name of the theme that you want to turn on:
|
||||
|
||||
```js
|
||||
window.REMARK42.changeTheme('light');
|
||||
window.REMARK42.changeTheme("light")
|
||||
```
|
||||
|
||||
#### Locales
|
||||
@@ -89,10 +90,10 @@ Add this snippet to the bottom of web page, or adjust already present `remark_co
|
||||
```html
|
||||
<script>
|
||||
var remark_config = {
|
||||
host: 'REMARK_URL',
|
||||
site_id: 'YOUR_SITE_ID',
|
||||
components: ['last-comments'],
|
||||
};
|
||||
host: "REMARK_URL",
|
||||
site_id: "YOUR_SITE_ID",
|
||||
components: ["last-comments"],
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -116,10 +117,10 @@ Add this snippet to the bottom of web page, or adjust already present `remark_co
|
||||
```html
|
||||
<script>
|
||||
var remark_config = {
|
||||
host: 'REMARK_URL',
|
||||
site_id: 'YOUR_SITE_ID',
|
||||
components: ['counter'],
|
||||
};
|
||||
host: "REMARK_URL",
|
||||
site_id: "YOUR_SITE_ID",
|
||||
components: ["counter"],
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -139,4 +140,3 @@ And then add a node like this in the place where you want to see a number of com
|
||||
You can use as many nodes like this as you need to. The script will find all of them by the class `remark__counter`, and it will use the `data-url` attribute to define the page with comments.
|
||||
|
||||
Also, the script can use `url` property from `remark_config` object or `window.location.origin + window.location.pathname` if nothing else is defined.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user