mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post-form): auto-convert YouTube links to thumbnail URLs
Add a countdown notice and shared hook to replace YouTube watch links with img.youtube.com thumbnails in post and reply forms, with i18n copy.
This commit is contained in:
@@ -621,6 +621,44 @@ describe('ReplyModal', () => {
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('moves YouTube links into reply content and publishes the thumbnail link on media-only boards', async () => {
|
||||
const youtubeLink = 'https://youtu.be/reply123';
|
||||
const thumbnailLink = 'https://img.youtube.com/vi/reply123/0.jpg';
|
||||
testState.openEmpty = true;
|
||||
testState.selectedText = 'reply body';
|
||||
testState.directoryByAddress['music-posting.eth'] = {
|
||||
address: 'music-posting.eth',
|
||||
features: { requirePostLinkIsMedia: true },
|
||||
title: '/mu/ - Music',
|
||||
};
|
||||
|
||||
await renderReplyModal('/mu/thread/post-1');
|
||||
|
||||
const textarea = container.querySelector<HTMLTextAreaElement>('textarea') as HTMLTextAreaElement;
|
||||
const linkInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[2];
|
||||
const linkContainer = linkInput.parentElement as HTMLElement;
|
||||
const getConversionNotice = () =>
|
||||
Array.from(container.querySelectorAll<HTMLDivElement>('div'))
|
||||
.reverse()
|
||||
.find((element) => element.textContent?.includes('youtube_thumbnail_link_conversion_notice'));
|
||||
|
||||
await dispatchInput(linkInput, youtubeLink);
|
||||
|
||||
expect(linkInput.value).toBe(youtubeLink);
|
||||
expect(container.textContent).toContain('youtube_thumbnail_link_conversion_notice:{"count":3}');
|
||||
expect(linkContainer.textContent).not.toContain('youtube_thumbnail_link_conversion_notice');
|
||||
expect(getConversionNotice()?.className).toContain('error');
|
||||
|
||||
await clickButtonByText('post');
|
||||
|
||||
expect(linkInput.value).toBe(thumbnailLink);
|
||||
expect(textarea.value).toBe(`${youtubeLink}\nreply body`);
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
||||
content: `${youtubeLink}\nreply body`,
|
||||
link: thumbnailLink,
|
||||
});
|
||||
});
|
||||
|
||||
it('validates unsupported options and stores fortune output in reply content', async () => {
|
||||
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.25);
|
||||
testState.openEmpty = true;
|
||||
|
||||
@@ -28,6 +28,7 @@ import { findDirectoryByAddress, useDirectories } from '../../hooks/use-director
|
||||
import usePublishReply from '../../hooks/use-publish-reply';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import { useFileUpload } from '../../hooks/use-file-upload';
|
||||
import { useYouTubeThumbnailLinkConversion } from '../../hooks/use-youtube-thumbnail-link-conversion';
|
||||
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';
|
||||
@@ -140,6 +141,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
);
|
||||
|
||||
const onPublishReply = () => {
|
||||
const appliedYouTubeConversion = applyPendingConversion();
|
||||
|
||||
const currentContent = textRef.current?.value || '';
|
||||
const currentUrl = urlRef.current?.value.trim() || '';
|
||||
const currentOptions = optionsRef.current?.value || '';
|
||||
@@ -180,7 +183,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
|
||||
setError(null);
|
||||
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? `/${postOptionsDirectoryCode || params.boardIdentifier || communityAddress}` : null;
|
||||
publishReply({ content: publishContent, ...flagPublishOptions });
|
||||
publishReply({ content: publishContent, ...(appliedYouTubeConversion ? { link: currentUrl } : {}), ...flagPublishOptions });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -391,6 +394,39 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
setIsBbcodePreviewing(true);
|
||||
};
|
||||
|
||||
const setLinkValue = (nextUrl: string) => {
|
||||
setUrl(nextUrl);
|
||||
setPublishReplyOptions({ link: nextUrl });
|
||||
};
|
||||
|
||||
const handleConvertedContentChange = (content: string) => {
|
||||
const nextSelection = content.length;
|
||||
if (textRef.current) {
|
||||
textRef.current.setSelectionRange(nextSelection, nextSelection);
|
||||
}
|
||||
handleContentValueChange(content, nextSelection, nextSelection);
|
||||
};
|
||||
|
||||
const {
|
||||
applyPendingConversion,
|
||||
cancelPendingConversion,
|
||||
noticeCountdown: youtubeThumbnailConversionCountdown,
|
||||
queueLinkConversion,
|
||||
} = useYouTubeThumbnailLinkConversion({
|
||||
enabled: requirePostLinkIsMedia,
|
||||
onContentChange: handleConvertedContentChange,
|
||||
onLinkChange: setLinkValue,
|
||||
textRef,
|
||||
urlRef,
|
||||
});
|
||||
|
||||
const handleLinkChange = (nextUrl: string) => {
|
||||
setLinkValue(nextUrl);
|
||||
if (queueLinkConversion(nextUrl)) {
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const canInsertQuote = showReplyModal && quoteInsertRequestId !== 0 && !!textRef.current;
|
||||
|
||||
@@ -438,25 +474,27 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const { isUploading, uploadedFileName, handleUpload, uploadFile } = useFileUpload({
|
||||
onUploadComplete: (uploadedUrl: string) => {
|
||||
if (uploadedUrl) {
|
||||
setUrl(uploadedUrl);
|
||||
cancelPendingConversion();
|
||||
setLinkValue(uploadedUrl);
|
||||
if (urlRef.current) {
|
||||
urlRef.current.value = uploadedUrl;
|
||||
}
|
||||
setPublishReplyOptions({ link: uploadedUrl });
|
||||
}
|
||||
},
|
||||
});
|
||||
const handleOekakiClearUploadedUrl = (uploadedUrl: string) => {
|
||||
if ((urlRef.current?.value || url) !== uploadedUrl) return;
|
||||
setUrl('');
|
||||
cancelPendingConversion();
|
||||
setLinkValue('');
|
||||
if (urlRef.current) {
|
||||
urlRef.current.value = '';
|
||||
}
|
||||
setPublishReplyOptions({ link: '' });
|
||||
};
|
||||
const uploadMode = useMediaHostingStore((state) => state.uploadMode);
|
||||
const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime());
|
||||
const displayedFileName = getPublishURLFilename(url) || uploadedFileName;
|
||||
const youtubeThumbnailConversionNotice =
|
||||
youtubeThumbnailConversionCountdown !== null ? t('youtube_thumbnail_link_conversion_notice', { count: youtubeThumbnailConversionCountdown }) : null;
|
||||
|
||||
const hasInitializedDisplayName = useRef(false);
|
||||
useEffect(() => {
|
||||
@@ -555,8 +593,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
placeholder={requirePostLinkIsMedia ? FILE_LINK_PLACEHOLDER : capitalize(t('link'))}
|
||||
disabled={isUploading}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
setPublishReplyOptions({ link: e.target.value });
|
||||
handleLinkChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -613,7 +650,11 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
</button>
|
||||
</div>
|
||||
{moderationPostingWarning ? <div className={styles.error}>{moderationPostingWarning}</div> : null}
|
||||
{lengthError ? (
|
||||
{youtubeThumbnailConversionNotice ? (
|
||||
<div className={styles.error} aria-live='polite'>
|
||||
{youtubeThumbnailConversionNotice}
|
||||
</div>
|
||||
) : lengthError ? (
|
||||
<div className={styles.error}>{lengthError}</div>
|
||||
) : error ? (
|
||||
<div className={styles.error}>{isPostOptionsValidationError(error) ? <PostOptionsErrorMessage error={error} directories={directories} /> : error}</div>
|
||||
|
||||
Reference in New Issue
Block a user