mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(posting): add scoped options and qst formatting (#1135)
* feat(posting): add scoped options and qst formatting * fix(posting): address post options review feedback * fix(posting): sync option content before publish * fix(posting): handle final review nits
This commit is contained in:
@@ -4,6 +4,7 @@ import { createRoot, type Root } from 'react-dom/client';
|
||||
import { Link, MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import PostForm, { LinkTypePreviewer } from '../post-form';
|
||||
import { POST_OPTIONS_VALIDATION_DELAY_MS } from '../../../lib/utils/post-options-utils';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
||||
@@ -110,6 +111,7 @@ vi.mock('../../../hooks/use-account-community-addresses', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../../hooks/use-directories', () => ({
|
||||
findDirectoryByAddress: (directories: typeof testState.directories, address: string | undefined) => directories.find((entry) => entry.address === address),
|
||||
useDirectories: () => testState.directories,
|
||||
useDirectoryByAddress: (address: string | undefined) => testState.directories.find((entry) => entry.address === address),
|
||||
normalizeBoardAddress: (address: string) => address.replace(/\.(bso|eth)$/, ''),
|
||||
@@ -159,10 +161,24 @@ vi.mock('../../../hooks/use-publish-post', async () => {
|
||||
}),
|
||||
[communityAddress],
|
||||
);
|
||||
const publishPost = React.useCallback(() => {
|
||||
testState.publishedPostOptions = getPublishPostOptions();
|
||||
return testState.publishPostMock();
|
||||
}, [getPublishPostOptions]);
|
||||
const publishPost = React.useCallback(
|
||||
(options?: Record<string, unknown>) => {
|
||||
const sanitizedOptions = Object.entries(options || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
acc[key] = value === '' ? undefined : value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
);
|
||||
testState.publishPostOptions = {
|
||||
...getPublishPostOptions(),
|
||||
...sanitizedOptions,
|
||||
};
|
||||
testState.publishedPostOptions = testState.publishPostOptions;
|
||||
return testState.publishPostMock(options);
|
||||
},
|
||||
[getPublishPostOptions],
|
||||
);
|
||||
const resetPublishPostOptions = React.useCallback(() => {
|
||||
testState.publishPostOptions = {};
|
||||
testState.resetPublishPostOptionsMock();
|
||||
@@ -209,7 +225,13 @@ vi.mock('../../../hooks/use-publish-reply', async () => {
|
||||
postCid: postCid ?? cid,
|
||||
communityAddress,
|
||||
});
|
||||
const publishReply = React.useCallback(() => testState.publishReplyMock(), []);
|
||||
const publishReply = React.useCallback((options?: Record<string, unknown>) => {
|
||||
if (options) {
|
||||
testState.setPublishReplyOptionsMock(options);
|
||||
setPublishReplyOptionsState((previous) => ({ ...previous, ...options }));
|
||||
}
|
||||
return testState.publishReplyMock(options);
|
||||
}, []);
|
||||
const resetPublishReplyOptions = React.useCallback(() => testState.resetPublishReplyOptionsMock(), []);
|
||||
const setPublishReplyOptions = React.useCallback((options: Record<string, unknown>) => {
|
||||
testState.setPublishReplyOptionsMock(options);
|
||||
@@ -274,9 +296,20 @@ vi.mock('../../../stores/use-media-hosting-store', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('lodash/debounce', () => ({
|
||||
default: <T extends (...args: any[]) => void>(fn: T) => {
|
||||
const wrapped = ((...args: Parameters<T>) => fn(...args)) as T & { cancel: () => void };
|
||||
wrapped.cancel = () => undefined;
|
||||
default: <T extends (...args: any[]) => void>(fn: T, wait = 0) => {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
const wrapped = ((...args: Parameters<T>) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => fn(...args), wait);
|
||||
}) as T & { cancel: () => void };
|
||||
wrapped.cancel = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = undefined;
|
||||
};
|
||||
return wrapped;
|
||||
},
|
||||
}));
|
||||
@@ -362,6 +395,12 @@ const dispatchInput = async (element: HTMLInputElement | HTMLTextAreaElement, va
|
||||
});
|
||||
};
|
||||
|
||||
const waitForOptionsValidation = async () => {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, POST_OPTIONS_VALIDATION_DELAY_MS + 20));
|
||||
});
|
||||
};
|
||||
|
||||
const dispatchChange = async (element: HTMLInputElement | HTMLSelectElement, value: string | boolean) => {
|
||||
await act(async () => {
|
||||
if (typeof value === 'boolean' && 'checked' in element) {
|
||||
@@ -385,6 +424,9 @@ describe('PostForm', () => {
|
||||
testState.comments = {};
|
||||
testState.directories = [
|
||||
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
|
||||
{ address: 'random-nsfw.bso', features: {}, title: '/b/ - Random' },
|
||||
{ address: 'silly-stuff.bso', features: {}, title: '/s5s/ - Silly Stuff' },
|
||||
{ address: 'traditional-games.bso', features: {}, title: '/tg/ - Traditional Games' },
|
||||
{ address: 'mod.eth', features: {}, title: '/mod/ - Moderation' },
|
||||
];
|
||||
testState.editedComment = undefined;
|
||||
@@ -408,6 +450,7 @@ describe('PostForm', () => {
|
||||
testState.uploadedFileName = 'picked.png';
|
||||
testState.communities = {
|
||||
'music-posting.eth': { address: 'music-posting.eth' },
|
||||
'traditional-games.bso': { address: 'traditional-games.bso' },
|
||||
};
|
||||
testState.handleUploadMock.mockReset();
|
||||
testState.navigateMock.mockReset();
|
||||
@@ -480,12 +523,14 @@ describe('PostForm', () => {
|
||||
|
||||
const textInputs = table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || [];
|
||||
const nameInput = textInputs[0];
|
||||
const subjectInput = textInputs[1];
|
||||
const linkInput = textInputs[2];
|
||||
const optionsInput = textInputs[1];
|
||||
const subjectInput = textInputs[2];
|
||||
const linkInput = textInputs[3];
|
||||
const textarea = table?.querySelector('textarea');
|
||||
const select = table?.querySelector('select');
|
||||
|
||||
expect(nameInput).toBeTruthy();
|
||||
expect(optionsInput?.getAttribute('aria-label')).toBe('options');
|
||||
expect(subjectInput).toBeTruthy();
|
||||
expect(linkInput).toBeTruthy();
|
||||
expect(linkInput?.getAttribute('placeholder')).toBe('https://website.com/image.jpg');
|
||||
@@ -546,7 +591,7 @@ describe('PostForm', () => {
|
||||
table = container.querySelector('table');
|
||||
textarea = table?.querySelector('textarea');
|
||||
const textInputs = table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || [];
|
||||
const linkInput = textInputs[2];
|
||||
const linkInput = textInputs[3];
|
||||
|
||||
expect(textarea?.value).toBe('');
|
||||
expect(linkInput).toBeTruthy();
|
||||
@@ -559,6 +604,140 @@ describe('PostForm', () => {
|
||||
expect(testState.publishedPostOptions?.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it('validates unsupported options and stores fortune output in post content', async () => {
|
||||
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.25);
|
||||
testState.resolvedCommunityAddress = 'random-nsfw.bso';
|
||||
|
||||
await renderPostForm('/b');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const optionsInput = table?.querySelector<HTMLInputElement>('input[aria-label="options"]');
|
||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
expect(optionsInput).toBeTruthy();
|
||||
expect(textarea).toBeTruthy();
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'x y z');
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
|
||||
await waitForOptionsValidation();
|
||||
expect(container.textContent).toContain('unsupported options: x, y, z');
|
||||
const delayedOptionsError = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'unsupported options: x, y, z');
|
||||
expect(delayedOptionsError?.className).toContain('error');
|
||||
expect(delayedOptionsError?.className).toContain('formError');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'fortune body');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).not.toHaveBeenCalled();
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
||||
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(testState.publishPostOptions.content).toBe('fortune body<span class="fortune" style="color:#fd4d32"><br><br><b>Your fortune: Excellent Luck</b></span>');
|
||||
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
|
||||
randomSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('supports fortune on the /s5s/ route when directory metadata is not loaded', async () => {
|
||||
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.25);
|
||||
testState.resolvedCommunityAddress = 'silly-stuff.bso';
|
||||
testState.directories = testState.directories.filter((entry) => entry.address !== 'silly-stuff.bso');
|
||||
|
||||
await renderPostForm('/s5s');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const optionsInput = table?.querySelector<HTMLInputElement>('input[aria-label="options"]');
|
||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
||||
await waitForOptionsValidation();
|
||||
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'silly fortune');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
|
||||
expect(testState.publishedPostOptions?.content).toBe('silly fortune<span class="fortune" style="color:#fd4d32"><br><br><b>Your fortune: Excellent Luck</b></span>');
|
||||
randomSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('stores dice rolls in post content on dice-enabled boards', async () => {
|
||||
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
||||
testState.resolvedCommunityAddress = 'quests.bso';
|
||||
|
||||
await renderPostForm('/qst');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const optionsInput = table?.querySelector<HTMLInputElement>('input[aria-label="options"]');
|
||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'dice+1d6+3');
|
||||
await waitForOptionsValidation();
|
||||
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'dice body');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
|
||||
expect(testState.publishedPostOptions?.content).toBe('<b>Rolled 4 + 3 = 7 (1d6 + 3)<br><br></b>dice body');
|
||||
randomSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('treats fortune as unsupported outside /b/ and /s5s/', async () => {
|
||||
testState.resolvedCommunityAddress = 'music-posting.eth';
|
||||
|
||||
await renderPostForm('/mu');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const optionsInput = table?.querySelector<HTMLInputElement>('input[aria-label="options"]');
|
||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
|
||||
await waitForOptionsValidation();
|
||||
expect(container.textContent).toContain('unsupported options: fortune');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'plain body');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).not.toHaveBeenCalled();
|
||||
expect(testState.publishPostOptions.content).toBe('plain body');
|
||||
});
|
||||
|
||||
it('treats dice rolls as unsupported outside /tg/ and /qst/', async () => {
|
||||
testState.resolvedCommunityAddress = 'random-nsfw.bso';
|
||||
|
||||
await renderPostForm('/b');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const optionsInput = table?.querySelector<HTMLInputElement>('input[aria-label="options"]');
|
||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'dice+1d6');
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
|
||||
await waitForOptionsValidation();
|
||||
expect(container.textContent).toContain('unsupported options: dice+1d6');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'plain dice');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
expect(testState.publishPostMock).not.toHaveBeenCalled();
|
||||
expect(testState.publishPostOptions.content).toBe('plain dice');
|
||||
});
|
||||
|
||||
it('shows BBCode controls only for board mods and inserts tags into the post textarea', async () => {
|
||||
testState.account = {
|
||||
author: { address: 'mod.eth', displayName: 'Alice' },
|
||||
@@ -653,7 +832,7 @@ describe('PostForm', () => {
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const textInputs = table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || [];
|
||||
const linkInput = textInputs[2];
|
||||
const linkInput = textInputs[3];
|
||||
|
||||
expect(table?.textContent).toContain('no_file_chosen');
|
||||
|
||||
@@ -691,7 +870,7 @@ describe('PostForm', () => {
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const textInputs = table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || [];
|
||||
const linkInput = textInputs[2];
|
||||
const linkInput = textInputs[3];
|
||||
const longFilename = 'TELEMMGLPICT000378070158_17159651831200_trans_NvBQzQNjv4BqpVlberWd9EgFPZtcLiMQf0Rf_Wk3V23H2268P_XkPxc.jpeg';
|
||||
|
||||
await dispatchInput(linkInput as HTMLInputElement, `https://www.telegraph.co.uk/multimedia/${longFilename}`);
|
||||
@@ -761,7 +940,7 @@ describe('PostForm', () => {
|
||||
expect(container.textContent).toContain('error: empty_comment_alert');
|
||||
|
||||
const textInputs = table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || [];
|
||||
const linkInput = textInputs[1];
|
||||
const linkInput = textInputs[2];
|
||||
expect(linkInput).toBeTruthy();
|
||||
|
||||
await dispatchInput(linkInput as HTMLInputElement, 'not-a-url');
|
||||
|
||||
@@ -7,6 +7,15 @@ import getShortAddress from '../../lib/get-short-address';
|
||||
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||
import { getDisplayMediaInfoType, getLinkMediaInfo } from '../../lib/utils/media-utils';
|
||||
import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation-utils';
|
||||
import {
|
||||
type DiceRoll,
|
||||
type FortuneEntry,
|
||||
POST_OPTIONS_VALIDATION_DELAY_MS,
|
||||
getContentWithPostOptionState as getContentWithOptions,
|
||||
getPostOptionsDirectoryCode,
|
||||
getUnsupportedPostOptionsMessage,
|
||||
isUnsupportedPostOptionsMessage,
|
||||
} from '../../lib/utils/post-options-utils';
|
||||
import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
|
||||
import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
|
||||
import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
|
||||
@@ -112,12 +121,14 @@ interface PostFormFieldsProps {
|
||||
isBbcodePreviewing: boolean;
|
||||
postCid: string;
|
||||
subjectRef: React.Ref<HTMLInputElement>;
|
||||
optionsRef: React.RefObject<HTMLInputElement>;
|
||||
textRef: React.RefObject<HTMLTextAreaElement>;
|
||||
urlRef: React.Ref<HTMLInputElement>;
|
||||
url: string;
|
||||
lengthError: string | null;
|
||||
handleContentChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
handleContentValueChange: (content: string) => void;
|
||||
handleContentValueChange: (content: string, options?: string) => void;
|
||||
handleOptionsChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
setPublishPostOptions: (opts: Record<string, unknown>) => void;
|
||||
setPublishReplyOptions: (opts: Record<string, unknown>) => void;
|
||||
setUrl: (url: string) => void;
|
||||
@@ -152,12 +163,14 @@ const PostFormFields = ({
|
||||
isBbcodePreviewing,
|
||||
postCid,
|
||||
subjectRef,
|
||||
optionsRef,
|
||||
textRef,
|
||||
urlRef,
|
||||
url,
|
||||
lengthError,
|
||||
handleContentChange,
|
||||
handleContentValueChange,
|
||||
handleOptionsChange,
|
||||
setPublishPostOptions,
|
||||
setPublishReplyOptions,
|
||||
setUrl,
|
||||
@@ -214,6 +227,12 @@ const PostFormFields = ({
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t('options')}</td>
|
||||
<td>
|
||||
<input type='text' aria-label={t('options')} ref={optionsRef} autoCorrect='off' autoComplete='off' spellCheck='false' onChange={handleOptionsChange} />
|
||||
</td>
|
||||
</tr>
|
||||
{!isInPostView && (
|
||||
<tr>
|
||||
<td>{t('subject')}</td>
|
||||
@@ -333,7 +352,7 @@ const PostFormFields = ({
|
||||
</tr>
|
||||
{((isInPostView && showSpoilerForReply) || (!isInPostView && showSpoilerForPost)) && (
|
||||
<tr className={styles.spoilerButton}>
|
||||
<td>{t('options')}</td>
|
||||
<td>{capitalize(t('spoiler'))}</td>
|
||||
<td>
|
||||
[
|
||||
<label>
|
||||
@@ -398,6 +417,9 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const textRef = useRef<HTMLTextAreaElement>(null);
|
||||
const urlRef = useRef<HTMLInputElement>(null);
|
||||
const subjectRef = useRef<HTMLInputElement>(null);
|
||||
const optionsRef = useRef<HTMLInputElement>(null);
|
||||
const fortuneEntryRef = useRef<FortuneEntry | null>(null);
|
||||
const diceRollRef = useRef<DiceRoll | null>(null);
|
||||
|
||||
const location = useLocation();
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
@@ -409,6 +431,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const rulesPath = effectiveBoardAddress ? `/rules/${getBoardPath(effectiveBoardAddress, directories)}` : '/rules';
|
||||
const showSpoilerForPost = directoryEntry?.features?.noSpoilers !== true;
|
||||
const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true;
|
||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
||||
const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView));
|
||||
|
||||
@@ -434,6 +457,15 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
}, 1000),
|
||||
).current;
|
||||
|
||||
const checkPostOptions = useRef(
|
||||
debounce((options: string, directoryCode: string | undefined) => {
|
||||
const nextOptionsError = getUnsupportedPostOptionsMessage(options, directoryCode);
|
||||
if (nextOptionsError) {
|
||||
setFormError(nextOptionsError);
|
||||
}
|
||||
}, POST_OPTIONS_VALIDATION_DELAY_MS),
|
||||
).current;
|
||||
|
||||
const resetFields = () => {
|
||||
if (textRef.current) {
|
||||
textRef.current.value = '';
|
||||
@@ -444,20 +476,36 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
if (subjectRef.current) {
|
||||
subjectRef.current.value = '';
|
||||
}
|
||||
if (optionsRef.current) {
|
||||
optionsRef.current.value = '';
|
||||
}
|
||||
checkContentLength.cancel();
|
||||
checkPostOptions.cancel();
|
||||
fortuneEntryRef.current = null;
|
||||
diceRollRef.current = null;
|
||||
setIsBbcodePreviewing(false);
|
||||
setBbcodePreviewContent('');
|
||||
};
|
||||
|
||||
const onPublishPost = () => {
|
||||
const currentTitle = subjectRef.current?.value.trim() || '';
|
||||
const currentContent = textRef.current?.value.trim() || '';
|
||||
const currentContent = textRef.current?.value || '';
|
||||
const currentUrl = urlRef.current?.value.trim() || '';
|
||||
const currentOptions = optionsRef.current?.value || '';
|
||||
const currentOptionsError = getUnsupportedPostOptionsMessage(currentOptions, postOptionsDirectoryCode);
|
||||
const publishContent = getContentWithOptions(currentContent, currentOptions, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||
|
||||
checkContentLength.cancel();
|
||||
checkPostOptions.cancel();
|
||||
setLengthError(null);
|
||||
setFormError(null);
|
||||
|
||||
if (!currentTitle && !currentContent && !currentUrl) {
|
||||
if (currentOptionsError) {
|
||||
setFormError(currentOptionsError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentTitle && !publishContent.trim() && !currentUrl) {
|
||||
setFormError(`${t('error')}: ${t('empty_comment_alert')}`);
|
||||
return;
|
||||
}
|
||||
@@ -471,7 +519,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentContent.length > 2000) {
|
||||
if (publishContent.trim().length > 2000) {
|
||||
setFormError(`${t('error')}: ${t('field_too_long')}`);
|
||||
return;
|
||||
}
|
||||
@@ -481,7 +529,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
return;
|
||||
}
|
||||
|
||||
publishPost();
|
||||
publishPost({ content: publishContent });
|
||||
};
|
||||
|
||||
// redirect to pending page when pending comment is created
|
||||
@@ -503,30 +551,39 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
checkContentLength.cancel();
|
||||
checkPostOptions.cancel();
|
||||
if (isInPostView) {
|
||||
resetPublishReplyOptions();
|
||||
} else {
|
||||
resetPublishPostOptions();
|
||||
}
|
||||
};
|
||||
}, [checkContentLength, isInPostView, resetPublishPostOptions, resetPublishReplyOptions]);
|
||||
}, [checkContentLength, checkPostOptions, isInPostView, resetPublishPostOptions, resetPublishReplyOptions]);
|
||||
|
||||
const handleContentValueChange = (content: string) => {
|
||||
const handleContentValueChange = (content: string, options = optionsRef.current?.value || '') => {
|
||||
const publishContent = getContentWithOptions(content, options, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||
if (isBbcodePreviewing) {
|
||||
setBbcodePreviewContent(content);
|
||||
}
|
||||
if (isInPostView) {
|
||||
setPublishReplyOptions({ content });
|
||||
setPublishReplyOptions({ content: publishContent });
|
||||
} else {
|
||||
setPublishPostOptions({ content });
|
||||
setPublishPostOptions({ content: publishContent });
|
||||
}
|
||||
checkContentLength(content, t);
|
||||
checkContentLength(publishContent, t);
|
||||
};
|
||||
|
||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
handleContentValueChange(e.target.value);
|
||||
};
|
||||
|
||||
const handleOptionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const options = e.target.value;
|
||||
handleContentValueChange(textRef.current?.value || '', options);
|
||||
setFormError((currentError) => (isUnsupportedPostOptionsMessage(currentError) ? null : currentError));
|
||||
checkPostOptions(options, postOptionsDirectoryCode);
|
||||
};
|
||||
|
||||
const handleBbcodePreviewToggle = () => {
|
||||
if (isBbcodePreviewing) {
|
||||
setIsBbcodePreviewing(false);
|
||||
@@ -539,14 +596,22 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
};
|
||||
|
||||
const onPublishReply = () => {
|
||||
const currentContent = textRef.current?.value.trim() || '';
|
||||
const currentUrl = urlRef.current?.value.trim() || '';
|
||||
const currentOptions = optionsRef.current?.value || '';
|
||||
const currentOptionsError = getUnsupportedPostOptionsMessage(currentOptions, postOptionsDirectoryCode);
|
||||
const publishContent = getContentWithOptions(textRef.current?.value || '', currentOptions, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||
|
||||
checkContentLength.cancel();
|
||||
checkPostOptions.cancel();
|
||||
setLengthError(null);
|
||||
setFormError(null);
|
||||
|
||||
if (!currentContent && !currentUrl) {
|
||||
if (currentOptionsError) {
|
||||
setFormError(currentOptionsError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!publishContent.trim() && !currentUrl) {
|
||||
setFormError(`${t('error')}: ${t('empty_comment_alert')}`);
|
||||
return;
|
||||
}
|
||||
@@ -561,12 +626,12 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentContent.length > 2000) {
|
||||
if (publishContent.trim().length > 2000) {
|
||||
setFormError(`${t('error')}: ${t('field_too_long')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
publishReply();
|
||||
publishReply({ content: publishContent });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -619,12 +684,14 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
isBbcodePreviewing={isBbcodePreviewing}
|
||||
postCid={postCid}
|
||||
subjectRef={subjectRef}
|
||||
optionsRef={optionsRef}
|
||||
textRef={textRef}
|
||||
urlRef={urlRef}
|
||||
url={url}
|
||||
lengthError={lengthError}
|
||||
handleContentChange={handleContentChange}
|
||||
handleContentValueChange={handleContentValueChange}
|
||||
handleOptionsChange={handleOptionsChange}
|
||||
setPublishPostOptions={setPublishPostOptions}
|
||||
setPublishReplyOptions={setPublishReplyOptions}
|
||||
setUrl={setUrl}
|
||||
|
||||
Reference in New Issue
Block a user