diff --git a/frontend/app/components/comment-form/comment-form.persist.ts b/frontend/app/components/comment-form/comment-form.persist.ts new file mode 100644 index 00000000..fd8e46f2 --- /dev/null +++ b/frontend/app/components/comment-form/comment-form.persist.ts @@ -0,0 +1,29 @@ +import { getJsonItem, updateJsonItem } from 'common/local-storage'; +import { LS_SAVED_COMMENT_VALUE } from 'common/constants'; + +export function getPersistedComments() { + return getJsonItem>(LS_SAVED_COMMENT_VALUE); +} + +export function getPersistedComment(id: string | undefined): string | undefined { + const comments = getPersistedComments(); + if (!comments || !id) { + return; + } + + return comments[id]; +} + +export function updatePersistedComments(id: string, value: string) { + updateJsonItem(LS_SAVED_COMMENT_VALUE, { ...getPersistedComments(), [id]: value }); +} + +export function removePersistedComment(id: string) { + updateJsonItem | null>(LS_SAVED_COMMENT_VALUE, (data) => { + if (!data) { + return null; + } + delete data[id]; + return data; + }); +} diff --git a/frontend/app/components/comment-form/comment-form.spec.tsx b/frontend/app/components/comment-form/comment-form.spec.tsx index 60dcef71..822dad52 100644 --- a/frontend/app/components/comment-form/comment-form.spec.tsx +++ b/frontend/app/components/comment-form/comment-form.spec.tsx @@ -4,12 +4,12 @@ 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'; +import { CommentForm, Props, messages } from './comment-form'; +import { updatePersistedComments, getPersistedComments } from './comment-form.persist'; -const user: CommentFormProps['user'] = { +const user: Props['user'] = { name: 'username', id: 'id_1', picture: '', @@ -19,10 +19,7 @@ const user: CommentFormProps['user'] = { verified: false, }; -function setup( - overrideProps: Partial = {}, - overrideConfig: Partial = {} -) { +function setup(overrideProps: Partial = {}, overrideConfig: Partial = {}) { Object.assign(StaticStore.config, overrideConfig); const props = { @@ -33,25 +30,25 @@ function setup( user: null, id: '1', ...overrideProps, - } as CommentFormProps; + } as Props; + const CommentFormWithIntl = () => ; return render(); } + describe('', () => { afterEach(() => { // reset textarea id in order to have `textarea_1` for every test CommentForm.textareaId = 0; + localStorage.clear(); }); 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 })); + updatePersistedComments('1', value); setup(); expect(screen.getByTestId('textarea_1')).toHaveValue(value); }); @@ -59,36 +56,33 @@ describe('', () => { it('should get initial value from localStorage', () => { const value = 'text'; - localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: value })); + updatePersistedComments('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' })); - + updatePersistedComments('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"}'); + expect(getPersistedComments()).toEqual({ '1': '1' }); fireEvent.input(screen.getByTestId('textarea_1'), { target: { value: '11' } }); - expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{"1":"11"}'); + expect(getPersistedComments()).toEqual({ '1': '11' }); }); it('should clear value after send', async () => { - localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: 'asd' })); + updatePersistedComments('1', 'asd'); const updateJsonItemSpy = jest.spyOn(localStorageModule, 'updateJsonItem'); setup(); @@ -96,7 +90,7 @@ describe('', () => { await waitFor(() => { expect(updateJsonItemSpy).toHaveBeenCalled(); }); - expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{}'); + expect(getPersistedComments()).toEqual({}); }); }); diff --git a/frontend/app/components/comment-form/comment-form.tsx b/frontend/app/components/comment-form/comment-form.tsx index 6158033c..7f96fc57 100644 --- a/frontend/app/components/comment-form/comment-form.tsx +++ b/frontend/app/components/comment-form/comment-form.tsx @@ -12,16 +12,16 @@ import { replaceSelection } from 'utils/replaceSelection'; import { Button } from 'components/button'; import { TextareaAutosize } from 'components/textarea-autosize'; import { Auth } from 'components/auth'; -import { getJsonItem, updateJsonItem } from 'common/local-storage'; -import { LS_SAVED_COMMENT_VALUE } from 'common/constants'; import { SubscribeByEmail } from './__subscribe-by-email'; import { SubscribeByRSS } from './__subscribe-by-rss'; import { MarkdownToolbar } from './markdown-toolbar'; import { TextExpander } from './text-expander'; +import { updatePersistedComments, getPersistedComment, removePersistedComment } from './comment-form.persist'; -export type CommentFormProps = { + +export type Props = { id: string; user: User | null; errorMessage?: string; @@ -39,7 +39,7 @@ export type CommentFormProps = { intl: IntlShape; }; -export type CommentFormState = { +export type State = { preview: string | null; isErrorShown: boolean; /** error message, if contains newlines, it will be split to multiple errors */ @@ -55,47 +55,7 @@ export type CommentFormState = { const ImageMimeRegex = /image\//i; -export const messages = defineMessages({ - placeholder: { - id: 'commentForm.input-placeholder', - defaultMessage: 'Your comment here', - }, - uploadFileFail: { - id: 'commentForm.upload-file-fail', - defaultMessage: '{fileName} upload failed with "{errorMessage}"', - }, - uploading: { - id: 'commentForm.uploading', - defaultMessage: 'Uploading...', - }, - uploadingFile: { - id: 'commentForm.uploading-file', - defaultMessage: 'uploading {fileName}...', - }, - exceededSize: { - id: 'commentForm.exceeded-size', - defaultMessage: '{fileName} exceeds size limit of {maxImageSize}', - }, - newComment: { - id: 'commentForm.new-comment', - defaultMessage: 'New comment', - }, - unexpectedError: { - id: 'commentForm.unexpected-error', - defaultMessage: 'Something went wrong. Please try again a bit later.', - }, - unauthorizedUploadingDisabled: { - id: 'commentForm.unauthorized-uploading-disabled', - defaultMessage: 'Image uploading is disabled for unauthorized users. You should login before uploading.', - }, - anonymousUploadingDisabled: { - id: 'commentForm.anonymous-uploading-disabled', - defaultMessage: - 'Image uploading is disabled for anonymous users. Please log in not as anonymous user to be able to attach images.', - }, -}); - -export class CommentForm extends Component { +export class CommentForm extends Component { /** reference to textarea element */ textareaRef = createRef(); static textareaId = 0; @@ -110,15 +70,15 @@ export class CommentForm extends Component { buttonText: null, }; - constructor(props: CommentFormProps) { + constructor(props: Props) { super(props); - const savedComments = getJsonItem>(LS_SAVED_COMMENT_VALUE); - this.state.text = props.value ?? savedComments?.[props.id] ?? ''; + const savedComment = getPersistedComment(props.id); + this.state.text = props.value ?? savedComment ?? ''; CommentForm.textareaId += 1; } - componentWillReceiveProps(nextProps: CommentFormProps) { + componentWillReceiveProps(nextProps: Props) { if (nextProps.value !== this.props.value) { this.setState({ text: nextProps.value || '' }); } @@ -130,7 +90,7 @@ export class CommentForm extends Component { } } - shouldComponentUpdate(nextProps: CommentFormProps, nextState: CommentFormState) { + shouldComponentUpdate(nextProps: Props, nextState: State) { const userId = this.props.user !== null && this.props.user.id; const nextUserId = nextProps.user !== null && nextProps.user.id; @@ -155,7 +115,7 @@ export class CommentForm extends Component { const { value } = e.target as HTMLInputElement; const text = value.substr(0, StaticStore.config.max_comment_size); - updateJsonItem(LS_SAVED_COMMENT_VALUE, { [this.props.id]: value }); + updatePersistedComments(this.props.id, value) if (this.state.errorLock) { this.setState({ @@ -206,13 +166,7 @@ export class CommentForm extends Component { return; } - updateJsonItem | null>(LS_SAVED_COMMENT_VALUE, (data) => { - if (data === null) { - return null; - } - delete data[this.props.id]; - return data; - }); + removePersistedComment(this.props.id) this.setState({ isDisabled: false, preview: null, text: '' }); }; @@ -368,8 +322,8 @@ export class CommentForm extends Component { continue; } - this.setState({ text: replaceSelection(this.state.text, selection, uploadPlaceholder) }, () => { - updateJsonItem(LS_SAVED_COMMENT_VALUE, { [this.props.id]: this.state.text }); + this.setState({ text: replaceSelection(this.state.text, selection, uploadPlaceholder) }, () => { + updatePersistedComments(this.props.id, this.state.text); }); !isFirst && (await sleep(uploadDelay)); @@ -391,8 +345,8 @@ export class CommentForm extends Component { markdownString ), }, - () => { - updateJsonItem(LS_SAVED_COMMENT_VALUE, { [this.props.id]: this.state.text }); + () => { + updatePersistedComments(this.props.id, this.state.text); } ); /** sleeping awhile so textarea catch state change and its selection */ @@ -566,3 +520,43 @@ export class CommentForm extends Component { ); } } + +export const messages = defineMessages({ + placeholder: { + id: 'commentForm.input-placeholder', + defaultMessage: 'Your comment here', + }, + uploadFileFail: { + id: 'commentForm.upload-file-fail', + defaultMessage: '{fileName} upload failed with "{errorMessage}"', + }, + uploading: { + id: 'commentForm.uploading', + defaultMessage: 'Uploading...', + }, + uploadingFile: { + id: 'commentForm.uploading-file', + defaultMessage: 'uploading {fileName}...', + }, + exceededSize: { + id: 'commentForm.exceeded-size', + defaultMessage: '{fileName} exceeds size limit of {maxImageSize}', + }, + newComment: { + id: 'commentForm.new-comment', + defaultMessage: 'New comment', + }, + unexpectedError: { + id: 'commentForm.unexpected-error', + defaultMessage: 'Something went wrong. Please try again a bit later.', + }, + unauthorizedUploadingDisabled: { + id: 'commentForm.unauthorized-uploading-disabled', + defaultMessage: 'Image uploading is disabled for unauthorized users. You should login before uploading.', + }, + anonymousUploadingDisabled: { + id: 'commentForm.anonymous-uploading-disabled', + defaultMessage: + 'Image uploading is disabled for anonymous users. Please log in not as anonymous user to be able to attach images.', + }, +}); diff --git a/frontend/app/components/comment-form/index.ts b/frontend/app/components/comment-form/index.ts index d6e47155..4791e61d 100644 --- a/frontend/app/components/comment-form/index.ts +++ b/frontend/app/components/comment-form/index.ts @@ -29,4 +29,4 @@ import './_theme/_light/comment-form_theme_light.css'; import './_simple/comment-form_simple.css'; export { CommentForm } from './comment-form'; -export type { CommentFormProps } from './comment-form'; +export type { Props } from './comment-form'; diff --git a/frontend/app/components/comment/comment.tsx b/frontend/app/components/comment/comment.tsx index 02f553be..84cc5bba 100644 --- a/frontend/app/components/comment/comment.tsx +++ b/frontend/app/components/comment/comment.tsx @@ -11,7 +11,7 @@ import { copy } from 'common/copy'; import { Theme, BlockTTL, Comment as CommentType, PostInfo, User, CommentMode, Profile } from 'common/types'; import { isUserAnonymous } from 'utils/isUserAnonymous'; -import { CommentFormProps } from 'components/comment-form'; +import { Props as CommentFormProps } from 'components/comment-form'; import { Avatar } from 'components/avatar'; import { VerificationIcon } from 'components/icons/verification'; import { getPreview, uploadImage } from 'common/api';