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 { MemoryRouter } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import ReplyModal from '../reply-modal';
|
||||
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;
|
||||
@@ -20,7 +21,9 @@ const testState = vi.hoisted(() => ({
|
||||
},
|
||||
} as Record<string, { address: string; directoryCode?: string; features?: Record<string, unknown>; title?: string }>,
|
||||
handleUploadMock: vi.fn(),
|
||||
uploadFileMock: vi.fn(),
|
||||
isMobile: false,
|
||||
mediaHostingRuntime: 'web' as 'web' | 'android' | 'electron',
|
||||
isResolvingExternalQuotes: false,
|
||||
isUploading: false,
|
||||
navigateMock: vi.fn(),
|
||||
@@ -127,8 +130,9 @@ vi.mock('../../../stores/use-reply-modal-store', () => ({
|
||||
}));
|
||||
|
||||
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', () => ({
|
||||
@@ -185,6 +189,7 @@ vi.mock('../../../hooks/use-file-upload', () => ({
|
||||
testState.uploadComplete = onUploadComplete;
|
||||
return {
|
||||
handleUpload: testState.handleUploadMock,
|
||||
uploadFile: testState.uploadFileMock,
|
||||
isUploading: testState.isUploading,
|
||||
uploadedFileName: testState.uploadedFileName,
|
||||
};
|
||||
@@ -352,6 +357,7 @@ describe('ReplyModal', () => {
|
||||
},
|
||||
};
|
||||
testState.handleUploadMock.mockReset();
|
||||
testState.uploadFileMock.mockReset();
|
||||
testState.isMobile = false;
|
||||
testState.isResolvingExternalQuotes = false;
|
||||
testState.isUploading = false;
|
||||
@@ -398,6 +404,7 @@ describe('ReplyModal', () => {
|
||||
testState.uploadComplete = undefined;
|
||||
testState.uploadedFileName = null;
|
||||
testState.uploadMode = 'always';
|
||||
testState.mediaHostingRuntime = 'web';
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
@@ -437,6 +444,30 @@ describe('ReplyModal', () => {
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ displayName: 'Alice' });
|
||||
});
|
||||
|
||||
it('shows Oekaki draw controls on /i/ replies', async () => {
|
||||
testState.directoryByAddress['oekaki-posting.bso'] = {
|
||||
address: 'oekaki-posting.bso',
|
||||
directoryCode: 'i',
|
||||
features: { requirePostLink: true, requirePostLinkIsMedia: true },
|
||||
title: '/i/ - Oekaki',
|
||||
};
|
||||
testState.communities['oekaki-posting.bso'] = { address: 'oekaki-posting.bso' };
|
||||
|
||||
await renderReplyModal('/i/thread/post-1', 'oekaki-posting.bso');
|
||||
|
||||
expect(container.textContent).toContain('Size');
|
||||
expect(container.textContent).toContain('Replay');
|
||||
expect(Array.from(container.querySelectorAll('span')).some((span) => span.textContent === '×')).toBe(true);
|
||||
expect(container.textContent).toContain(OEKAKI_WEB_WARNING_TEXT);
|
||||
expect(Array.from(container.querySelectorAll('button')).some((button) => button.textContent === 'Draw')).toBe(true);
|
||||
expect((Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Clear') as HTMLButtonElement | undefined)?.disabled).toBe(true);
|
||||
|
||||
testState.mediaHostingRuntime = 'android';
|
||||
await renderReplyModal('/i/thread/post-1', 'oekaki-posting.bso');
|
||||
|
||||
expect(container.textContent).not.toContain(OEKAKI_WEB_WARNING_TEXT);
|
||||
});
|
||||
|
||||
it('shows a flag selector on flag boards and publishes the default geographic request', async () => {
|
||||
await renderReplyModal('/pol/thread/post-1', 'politically-incorrect.bso');
|
||||
|
||||
|
||||
@@ -120,6 +120,32 @@
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.oekakiRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 302px;
|
||||
margin-bottom: 1px;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.oekakiLabel {
|
||||
display: inline-block;
|
||||
width: 38px;
|
||||
padding-top: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.oekakiControls {
|
||||
width: 264px;
|
||||
}
|
||||
|
||||
.oekakiWarning {
|
||||
width: 294px;
|
||||
margin-bottom: 1px;
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.offlineBoard {
|
||||
width: 292px;
|
||||
font-family: monospace;
|
||||
|
||||
@@ -28,9 +28,11 @@ import usePublishReply from '../../hooks/use-publish-reply';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import { useFileUpload } from '../../hooks/use-file-upload';
|
||||
import { useCommunityField } from '../../hooks/use-stable-community';
|
||||
import { OEKAKI_WEB_WARNING_TEXT } from '../../lib/oekaki/oekaki-copy';
|
||||
import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
|
||||
import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
|
||||
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 './reply-modal.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
@@ -63,6 +65,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const directoryEntry = findDirectoryByAddress(directories, communityAddress);
|
||||
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);
|
||||
@@ -429,7 +432,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
checkContentLengthRef.current(publishContent, t);
|
||||
}, [showReplyModal, quoteInsertRequestId, quoteInsertNumber, quoteInsertSelectedText, postOptionsDirectoryCode, setPublishReplyOptions, t]);
|
||||
|
||||
const { isUploading, uploadedFileName, handleUpload } = useFileUpload({
|
||||
const { isUploading, uploadedFileName, handleUpload, uploadFile } = useFileUpload({
|
||||
onUploadComplete: (uploadedUrl: string) => {
|
||||
if (uploadedUrl) {
|
||||
setUrl(uploadedUrl);
|
||||
@@ -440,6 +443,14 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
}
|
||||
},
|
||||
});
|
||||
const handleOekakiClearUploadedUrl = (uploadedUrl: string) => {
|
||||
if ((urlRef.current?.value || url) !== uploadedUrl) return;
|
||||
setUrl('');
|
||||
if (urlRef.current) {
|
||||
urlRef.current.value = '';
|
||||
}
|
||||
setPublishReplyOptions({ link: '' });
|
||||
};
|
||||
const uploadMode = useMediaHostingStore((state) => state.uploadMode);
|
||||
const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime());
|
||||
const displayedFileName = getPublishURLFilename(url) || uploadedFileName;
|
||||
@@ -546,6 +557,13 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{showOekakiControls && (
|
||||
<div className={styles.oekakiRow}>
|
||||
<span className={styles.oekakiLabel}>Draw</span>
|
||||
<OekakiDrawingControls className={styles.oekakiControls} disabled={isUploading} uploadFile={uploadFile} onClearUploadedUrl={handleOekakiClearUploadedUrl} />
|
||||
</div>
|
||||
)}
|
||||
{showOekakiControls && isWebRuntime() ? <div className={styles.oekakiWarning}>{OEKAKI_WEB_WARNING_TEXT}</div> : null}
|
||||
{flagOptions.length > 0 && (
|
||||
<div>
|
||||
<select
|
||||
|
||||
Reference in New Issue
Block a user