mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(oekaki): add drawing flow for /i/ (#1144)
* feat(oekaki): add drawing flow for /i/ * fix(oekaki): address review feedback * fix(oekaki): reset Tegaki edit sessions * fix(oekaki): block drawing during export * fix(oekaki): destroy Tegaki on preload errors * fix(oekaki): preserve drawing on export failure * fix(oekaki): unlock controls after export failure
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 { OEKAKI_WEB_WARNING_TEXT } from '../../../lib/oekaki/oekaki-copy';
|
||||
import { POST_OPTIONS_VALIDATION_DELAY_MS } from '../../../lib/utils/post-options-utils';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
@@ -24,10 +25,12 @@ const testState = vi.hoisted(() => ({
|
||||
editedComment: undefined as { commentModeration?: { archived?: boolean }; deleted?: boolean; locked?: boolean; postCid?: string; removed?: boolean } | undefined,
|
||||
gifFrameStatus: 'idle' as 'idle' | 'ready',
|
||||
handleUploadMock: vi.fn(),
|
||||
uploadFileMock: vi.fn(),
|
||||
isOffline: false,
|
||||
isOnlineStatusLoading: false,
|
||||
isUploading: false,
|
||||
isResolvingExternalQuotes: false,
|
||||
mediaHostingRuntime: 'web' as 'web' | 'android' | 'electron',
|
||||
navigateMock: vi.fn(),
|
||||
offlineTitle: 'offline board',
|
||||
postIndex: undefined as number | undefined,
|
||||
@@ -259,6 +262,7 @@ vi.mock('../../../hooks/use-file-upload', () => ({
|
||||
testState.uploadComplete = onUploadComplete;
|
||||
return {
|
||||
handleUpload: testState.handleUploadMock,
|
||||
uploadFile: testState.uploadFileMock,
|
||||
isUploading: testState.isUploading,
|
||||
uploadedFileName: testState.uploadedFileName,
|
||||
};
|
||||
@@ -286,8 +290,9 @@ vi.mock('../../../lib/utils/media-utils', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/media-hosting/show-upload-controls', () => ({
|
||||
getMediaHostingRuntime: () => testState.mediaHostingRuntime,
|
||||
getShowUploadControls: () => testState.showUploadControls,
|
||||
isWebRuntime: () => true,
|
||||
isWebRuntime: () => testState.mediaHostingRuntime === 'web',
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores/use-media-hosting-store', () => ({
|
||||
@@ -438,6 +443,7 @@ describe('PostForm', () => {
|
||||
testState.isOnlineStatusLoading = false;
|
||||
testState.isUploading = false;
|
||||
testState.isResolvingExternalQuotes = false;
|
||||
testState.mediaHostingRuntime = 'web';
|
||||
testState.offlineTitle = 'offline board';
|
||||
testState.postIndex = undefined;
|
||||
testState.publishedPostOptions = undefined;
|
||||
@@ -456,6 +462,7 @@ describe('PostForm', () => {
|
||||
'traditional-games.bso': { address: 'traditional-games.bso' },
|
||||
};
|
||||
testState.handleUploadMock.mockReset();
|
||||
testState.uploadFileMock.mockReset();
|
||||
testState.navigateMock.mockReset();
|
||||
testState.publishPostMock.mockReset();
|
||||
testState.publishReplyMock.mockReset();
|
||||
@@ -573,6 +580,43 @@ describe('PostForm', () => {
|
||||
expect(testState.setPublishPostOptionsMock).toHaveBeenCalledWith({ communityAddress: 'music-posting.eth' });
|
||||
});
|
||||
|
||||
it('shows Oekaki draw controls only on the /i/ board form', async () => {
|
||||
testState.directories.push({
|
||||
address: 'oekaki-posting.bso',
|
||||
directoryCode: 'i',
|
||||
features: { requirePostLink: true, requirePostLinkIsMedia: true },
|
||||
title: '/i/ - Oekaki',
|
||||
});
|
||||
testState.communities['oekaki-posting.bso'] = { address: 'oekaki-posting.bso' };
|
||||
testState.resolvedCommunityAddress = 'oekaki-posting.bso';
|
||||
|
||||
await renderPostForm('/i');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table') as HTMLTableElement;
|
||||
const drawRow = Array.from(table.querySelectorAll('tr')).find((row) => row.textContent?.includes('Size') && row.textContent?.includes('Replay'));
|
||||
expect(table.textContent).toContain('Size');
|
||||
expect(table.textContent).toContain('Replay');
|
||||
expect(Array.from(table.querySelectorAll('span')).some((span) => span.textContent === '×')).toBe(true);
|
||||
expect(drawRow?.textContent).not.toContain(OEKAKI_WEB_WARNING_TEXT);
|
||||
expect(Array.from(table.querySelectorAll('button')).some((button) => button.textContent === 'Draw')).toBe(true);
|
||||
expect((Array.from(table.querySelectorAll('button')).find((button) => button.textContent === 'Clear') as HTMLButtonElement | undefined)?.disabled).toBe(true);
|
||||
const rulesItems = Array.from(table.querySelectorAll('tr.rules li')).map((item) => item.textContent);
|
||||
expect(rulesItems).toEqual(['Please read the Rules and FAQ before posting.', OEKAKI_WEB_WARNING_TEXT]);
|
||||
|
||||
testState.mediaHostingRuntime = 'electron';
|
||||
await renderPostForm('/i');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
expect(container.textContent).not.toContain(OEKAKI_WEB_WARNING_TEXT);
|
||||
|
||||
testState.resolvedCommunityAddress = 'music-posting.eth';
|
||||
await renderPostForm('/mu');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
expect(Array.from(container.querySelectorAll('button')).some((button) => button.textContent === 'Draw')).toBe(false);
|
||||
});
|
||||
|
||||
it('drops stale thread content when board navigation remounts the form before a link-only post', async () => {
|
||||
await renderNavigablePostForm('/mu');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
@@ -36,11 +36,13 @@ import usePublishPost from '../../hooks/use-publish-post';
|
||||
import usePublishReply from '../../hooks/use-publish-reply';
|
||||
import { useFileUpload } from '../../hooks/use-file-upload';
|
||||
import { getShowUploadControls, isWebRuntime } from '../../lib/media-hosting/show-upload-controls';
|
||||
import { OEKAKI_WEB_WARNING_TEXT } from '../../lib/oekaki/oekaki-copy';
|
||||
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
||||
import useMediaHostingStore from '../../stores/use-media-hosting-store';
|
||||
import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
|
||||
import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
|
||||
import LoadingEllipsis from '../loading-ellipsis';
|
||||
import OekakiDrawingControls from '../oekaki-drawing-controls';
|
||||
import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message';
|
||||
import styles from './post-form.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
@@ -141,6 +143,7 @@ interface PostFormFieldsProps {
|
||||
isUploading: boolean;
|
||||
uploadedFileName: string | null | undefined;
|
||||
showUploadControls: boolean;
|
||||
showOekakiControls: boolean;
|
||||
showSpoilerForPost: boolean;
|
||||
showSpoilerForReply: boolean;
|
||||
isInAllView: boolean;
|
||||
@@ -158,6 +161,8 @@ interface PostFormFieldsProps {
|
||||
onPublishReply: () => void;
|
||||
onPublishPost: () => void;
|
||||
handleUpload: () => void;
|
||||
uploadFile: ReturnType<typeof useFileUpload>['uploadFile'];
|
||||
onOekakiClearUploadedUrl: (url: string) => void;
|
||||
disableReplyPublish: boolean;
|
||||
}
|
||||
|
||||
@@ -185,6 +190,7 @@ const PostFormFields = ({
|
||||
isUploading,
|
||||
uploadedFileName,
|
||||
showUploadControls,
|
||||
showOekakiControls,
|
||||
showSpoilerForPost,
|
||||
showSpoilerForReply,
|
||||
isInAllView,
|
||||
@@ -202,6 +208,8 @@ const PostFormFields = ({
|
||||
onPublishReply,
|
||||
onPublishPost,
|
||||
handleUpload,
|
||||
uploadFile,
|
||||
onOekakiClearUploadedUrl,
|
||||
disableReplyPublish,
|
||||
}: PostFormFieldsProps) => (
|
||||
<>
|
||||
@@ -365,6 +373,14 @@ const PostFormFields = ({
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{showOekakiControls && (
|
||||
<tr>
|
||||
<td>Draw</td>
|
||||
<td>
|
||||
<OekakiDrawingControls disabled={isUploading} uploadFile={uploadFile} onClearUploadedUrl={onOekakiClearUploadedUrl} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{((isInPostView && showSpoilerForReply) || (!isInPostView && showSpoilerForPost)) && (
|
||||
<tr className={styles.spoilerButton}>
|
||||
<td>{capitalize(t('spoiler'))}</td>
|
||||
@@ -424,6 +440,7 @@ const PostFormFields = ({
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
{showOekakiControls && isWebRuntime() ? <li>{OEKAKI_WEB_WARNING_TEXT}</li> : null}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -455,6 +472,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const nonokoRedirectPathRef = useRef<string | null>(null);
|
||||
|
||||
const location = useLocation();
|
||||
const isInPostView = isPostPageView(location.pathname, params);
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInModView = isModView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||
@@ -466,6 +484,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const showSpoilerForPost = directoryEntry?.features?.noSpoilers !== true;
|
||||
const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true;
|
||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||
const showOekakiControls = postOptionsDirectoryCode === 'i' || directoryEntry?.directoryCode === 'i';
|
||||
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
||||
const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView));
|
||||
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
|
||||
@@ -600,7 +619,6 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
}, [postIndex, pendingPostBoardPath, resetFields, resetPublishPostOptions, navigate]);
|
||||
|
||||
// in post page, publish a reply to the post
|
||||
const isInPostView = isPostPageView(location.pathname, params);
|
||||
const cid = params?.commentCid || '';
|
||||
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
|
||||
usePublishReply({ cid, communityAddress, postCid });
|
||||
@@ -707,7 +725,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
}
|
||||
}, [replyIndex, closeForm, navigate, resetFields]);
|
||||
|
||||
const { isUploading, uploadedFileName, handleUpload } = useFileUpload({
|
||||
const { isUploading, uploadedFileName, handleUpload, uploadFile } = useFileUpload({
|
||||
onUploadComplete: (uploadedUrl: string) => {
|
||||
if (uploadedUrl) {
|
||||
setUrl(uploadedUrl);
|
||||
@@ -722,6 +740,21 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
}
|
||||
},
|
||||
});
|
||||
const handleOekakiClearUploadedUrl = useCallback(
|
||||
(uploadedUrl: string) => {
|
||||
if ((urlRef.current?.value || url) !== uploadedUrl) return;
|
||||
setUrl('');
|
||||
if (urlRef.current) {
|
||||
urlRef.current.value = '';
|
||||
}
|
||||
if (isInPostView) {
|
||||
setPublishReplyOptions({ link: '' });
|
||||
} else {
|
||||
setPublishPostOptions({ link: '' });
|
||||
}
|
||||
},
|
||||
[isInPostView, setPublishPostOptions, setPublishReplyOptions, url],
|
||||
);
|
||||
const uploadMode = useMediaHostingStore((state) => state.uploadMode);
|
||||
const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime());
|
||||
|
||||
@@ -765,6 +798,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
isUploading={isUploading}
|
||||
uploadedFileName={uploadedFileName}
|
||||
showUploadControls={showUploadControls}
|
||||
showOekakiControls={showOekakiControls}
|
||||
showSpoilerForPost={showSpoilerForPost}
|
||||
showSpoilerForReply={showSpoilerForReply}
|
||||
isInAllView={isInAllView}
|
||||
@@ -782,6 +816,8 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
onPublishReply={onPublishReply}
|
||||
onPublishPost={onPublishPost}
|
||||
handleUpload={handleUpload}
|
||||
uploadFile={uploadFile}
|
||||
onOekakiClearUploadedUrl={handleOekakiClearUploadedUrl}
|
||||
disableReplyPublish={isResolvingExternalQuotes}
|
||||
/>
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user