mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(reply modal): insert YouTube links at cursor and enable conversion everywhere
YouTube thumbnail conversion now inserts links at the textarea caret instead of prepending, runs in reply modals even when media links are optional, and preserves the YouTube brand label on desktop posts.
This commit is contained in:
@@ -163,8 +163,10 @@ vi.mock('../../lib/utils/media-utils', () => ({
|
|||||||
getDisplayMediaInfoType: (type?: string) => type ?? 'unknown',
|
getDisplayMediaInfoType: (type?: string) => type ?? 'unknown',
|
||||||
getHasThumbnail: () => true,
|
getHasThumbnail: () => true,
|
||||||
getMediaDimensions: () => '100x100',
|
getMediaDimensions: () => '100x100',
|
||||||
getPostMediaTypeLabel: (_commentMediaInfo: unknown, resolvedType?: string) => resolvedType ?? '',
|
getPostMediaTypeLabel: (commentMediaInfo: { url?: string } | undefined, resolvedType?: string) =>
|
||||||
getYouTubeEmbedPostMediaFileLink: () => undefined,
|
commentMediaInfo?.url?.includes('youtube.com') ? 'YouTube video' : (resolvedType ?? ''),
|
||||||
|
getYouTubeEmbedPostMediaFileLink: (commentMediaInfo: { url?: string } | undefined) =>
|
||||||
|
commentMediaInfo?.url?.includes('youtube.com') ? 'https://img.youtube.com/vi/abc123/maxresdefault.jpg' : undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../lib/utils/post-utils', () => ({
|
vi.mock('../../lib/utils/post-utils', () => ({
|
||||||
@@ -227,7 +229,14 @@ vi.mock('../../hooks/use-author-address-click', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../hooks/use-comment-media-info', () => ({
|
vi.mock('../../hooks/use-comment-media-info', () => ({
|
||||||
useCommentMediaInfo: (link?: string) => (link ? { type: 'image', url: link } : undefined),
|
useCommentMediaInfo: (link?: string) =>
|
||||||
|
link
|
||||||
|
? {
|
||||||
|
patternThumbnailUrl: link.includes('youtube.com') ? 'https://img.youtube.com/vi/abc123/maxresdefault.jpg' : undefined,
|
||||||
|
type: link.includes('youtube.com') ? 'iframe' : 'image',
|
||||||
|
url: link,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../hooks/use-count-links-in-replies', () => ({
|
vi.mock('../../hooks/use-count-links-in-replies', () => ({
|
||||||
@@ -495,6 +504,18 @@ describe('post community address compatibility', () => {
|
|||||||
expect(container.textContent).toContain('reply-1');
|
expect(container.textContent).toContain('reply-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders YouTube media labels without splitting the brand name on desktop', async () => {
|
||||||
|
const post = {
|
||||||
|
...makeLegacyThreadWithoutReplies(),
|
||||||
|
link: 'https://www.youtube.com/watch?v=abc123',
|
||||||
|
};
|
||||||
|
|
||||||
|
await renderWithRoute(createElement(PostDesktop, { post }), '/mu/thread/post-1');
|
||||||
|
|
||||||
|
expect(container.textContent).toContain('(youtube video, 100x100)');
|
||||||
|
expect(container.textContent).not.toContain('you tube video');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders mobile multiboard posts with only communityAddress and still fetches replies', async () => {
|
it('renders mobile multiboard posts with only communityAddress and still fetches replies', async () => {
|
||||||
await renderWithRoute(createElement(PostMobile, { post: makeLegacyThread() }));
|
await renderWithRoute(createElement(PostMobile, { post: makeLegacyThread() }));
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import TimeAgoTooltip from '../time-ago-tooltip';
|
|||||||
import { PostProps } from '../../views/post/post';
|
import { PostProps } from '../../views/post/post';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
import lowerCase from 'lodash/lowerCase';
|
|
||||||
import { shouldShowSnow } from '../../lib/snow';
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||||
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
import { selectPostMenuProps } from '../../lib/utils/post-menu-props';
|
||||||
@@ -582,7 +581,7 @@ const PostMedia = ({
|
|||||||
const directoryEntry = findDirectoryByAddress(directories, communityAddress);
|
const directoryEntry = findDirectoryByAddress(directories, communityAddress);
|
||||||
const requirePostLinkIsMedia = directoryEntry?.features?.requirePostLinkIsMedia === true;
|
const requirePostLinkIsMedia = directoryEntry?.features?.requirePostLinkIsMedia === true;
|
||||||
const fileLabel = youtubeFileLink || requirePostLinkIsMedia ? t('file') : t('link');
|
const fileLabel = youtubeFileLink || requirePostLinkIsMedia ? t('file') : t('link');
|
||||||
const mediaTypeLabel = type ? lowerCase(getPostMediaTypeLabel(commentMediaInfo, type, t)) : '';
|
const mediaTypeLabel = type ? getPostMediaTypeLabel(commentMediaInfo, type, t).toLowerCase() : '';
|
||||||
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined;
|
const boardPath = communityAddress ? getBoardPath(communityAddress, directories) : undefined;
|
||||||
const displayBoardPath =
|
const displayBoardPath =
|
||||||
boardPath && communityAddress && boardPath !== communityAddress
|
boardPath && communityAddress && boardPath !== communityAddress
|
||||||
|
|||||||
@@ -735,6 +735,8 @@ describe('PostForm', () => {
|
|||||||
|
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
try {
|
try {
|
||||||
|
await dispatchInput(textarea, 'test');
|
||||||
|
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
||||||
await dispatchInput(linkInput, youtubeLink);
|
await dispatchInput(linkInput, youtubeLink);
|
||||||
|
|
||||||
expect(linkInput.value).toBe(youtubeLink);
|
expect(linkInput.value).toBe(youtubeLink);
|
||||||
@@ -763,7 +765,7 @@ describe('PostForm', () => {
|
|||||||
|
|
||||||
expect(linkInput.value).toBe(thumbnailLink);
|
expect(linkInput.value).toBe(thumbnailLink);
|
||||||
expect(linkInput.disabled).toBe(false);
|
expect(linkInput.disabled).toBe(false);
|
||||||
expect(textarea.value).toBe(youtubeLink);
|
expect(textarea.value).toBe(`test ${youtubeLink}`);
|
||||||
expect(container.textContent).not.toContain('youtube_thumbnail_link_conversion_notice');
|
expect(container.textContent).not.toContain('youtube_thumbnail_link_conversion_notice');
|
||||||
} finally {
|
} finally {
|
||||||
vi.clearAllTimers();
|
vi.clearAllTimers();
|
||||||
@@ -774,7 +776,7 @@ describe('PostForm', () => {
|
|||||||
|
|
||||||
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
|
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
|
||||||
expect(testState.publishedPostOptions?.link).toBe(thumbnailLink);
|
expect(testState.publishedPostOptions?.link).toBe(thumbnailLink);
|
||||||
expect(testState.publishedPostOptions?.content).toBe(youtubeLink);
|
expect(testState.publishedPostOptions?.content).toBe(`test ${youtubeLink}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requires a link when live community features require post links', async () => {
|
it('requires a link when live community features require post links', async () => {
|
||||||
@@ -901,6 +903,7 @@ describe('PostForm', () => {
|
|||||||
|
|
||||||
await dispatchChange(select, 'music-posting.eth');
|
await dispatchChange(select, 'music-posting.eth');
|
||||||
await dispatchInput(textarea, 'Video body');
|
await dispatchInput(textarea, 'Video body');
|
||||||
|
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
||||||
await dispatchInput(linkInput, youtubeLink);
|
await dispatchInput(linkInput, youtubeLink);
|
||||||
|
|
||||||
await clickByText(table, 'post');
|
await clickByText(table, 'post');
|
||||||
@@ -918,7 +921,7 @@ describe('PostForm', () => {
|
|||||||
|
|
||||||
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
|
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
|
||||||
expect(testState.publishedPostOptions?.link).toBe(thumbnailLink);
|
expect(testState.publishedPostOptions?.link).toBe(thumbnailLink);
|
||||||
expect(testState.publishedPostOptions?.content).toBe(`${youtubeLink}\nVideo body`);
|
expect(testState.publishedPostOptions?.content).toBe(`Video body ${youtubeLink}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores duplicate post clicks while publish is pending', async () => {
|
it('ignores duplicate post clicks while publish is pending', async () => {
|
||||||
|
|||||||
@@ -787,6 +787,13 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
checkContentLength(publishContent, t, options, postOptionsDirectoryCode);
|
checkContentLength(publishContent, t, options, postOptionsDirectoryCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleConvertedContentChange = (content: string, selectionStart: number, selectionEnd: number) => {
|
||||||
|
if (textRef.current) {
|
||||||
|
textRef.current.setSelectionRange(selectionStart, selectionEnd);
|
||||||
|
}
|
||||||
|
handleContentValueChange(content);
|
||||||
|
};
|
||||||
|
|
||||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
handleContentValueChange(e.target.value);
|
handleContentValueChange(e.target.value);
|
||||||
};
|
};
|
||||||
@@ -881,7 +888,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
queueLinkConversion,
|
queueLinkConversion,
|
||||||
} = useYouTubeThumbnailLinkConversion({
|
} = useYouTubeThumbnailLinkConversion({
|
||||||
enabled: requireCurrentLinkIsMedia && !(isInPostView && noReplyLinks),
|
enabled: requireCurrentLinkIsMedia && !(isInPostView && noReplyLinks),
|
||||||
onContentChange: handleContentValueChange,
|
onContentChange: handleConvertedContentChange,
|
||||||
onLinkChange: setLinkValue,
|
onLinkChange: setLinkValue,
|
||||||
textRef,
|
textRef,
|
||||||
urlRef,
|
urlRef,
|
||||||
|
|||||||
@@ -938,6 +938,9 @@ describe('ReplyModal', () => {
|
|||||||
.reverse()
|
.reverse()
|
||||||
.find((element) => element.textContent?.includes('youtube_thumbnail_link_conversion_notice'));
|
.find((element) => element.textContent?.includes('youtube_thumbnail_link_conversion_notice'));
|
||||||
|
|
||||||
|
const insertionPoint = 'reply'.length;
|
||||||
|
textarea.setSelectionRange(insertionPoint, insertionPoint);
|
||||||
|
|
||||||
await dispatchInput(linkInput, youtubeLink);
|
await dispatchInput(linkInput, youtubeLink);
|
||||||
|
|
||||||
expect(linkInput.value).toBe(youtubeLink);
|
expect(linkInput.value).toBe(youtubeLink);
|
||||||
@@ -948,13 +951,64 @@ describe('ReplyModal', () => {
|
|||||||
await clickButtonByText('post');
|
await clickButtonByText('post');
|
||||||
|
|
||||||
expect(linkInput.value).toBe(thumbnailLink);
|
expect(linkInput.value).toBe(thumbnailLink);
|
||||||
expect(textarea.value).toBe(`${youtubeLink}\nreply body`);
|
expect(textarea.value).toBe(`reply ${youtubeLink} body`);
|
||||||
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
||||||
content: `${youtubeLink}\nreply body`,
|
content: `reply ${youtubeLink} body`,
|
||||||
link: thumbnailLink,
|
link: thumbnailLink,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('automatically moves YouTube links into reply content when reply media links are optional', async () => {
|
||||||
|
const youtubeLink = 'https://youtu.be/replyoptional';
|
||||||
|
const thumbnailLink = 'https://img.youtube.com/vi/replyoptional/maxresdefault.jpg';
|
||||||
|
testState.openEmpty = true;
|
||||||
|
testState.selectedText = 'reply body';
|
||||||
|
|
||||||
|
await renderReplyModal('/mu/thread/post-1');
|
||||||
|
|
||||||
|
const textarea = container.querySelector<HTMLTextAreaElement>('textarea') as HTMLTextAreaElement;
|
||||||
|
const linkInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[2];
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
await dispatchInput(linkInput, youtubeLink);
|
||||||
|
|
||||||
|
expect(linkInput.value).toBe(youtubeLink);
|
||||||
|
expect(linkInput.disabled).toBe(true);
|
||||||
|
expect(container.textContent).toContain('youtube_thumbnail_link_conversion_notice:{"count":3}');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
expect(container.textContent).toContain('youtube_thumbnail_link_conversion_notice:{"count":2}');
|
||||||
|
expect(linkInput.disabled).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
expect(container.textContent).toContain('youtube_thumbnail_link_conversion_notice:{"count":1}');
|
||||||
|
expect(linkInput.disabled).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(linkInput.value).toBe(thumbnailLink);
|
||||||
|
expect(linkInput.disabled).toBe(false);
|
||||||
|
expect(textarea.value).toBe(`reply body ${youtubeLink}`);
|
||||||
|
expect(container.textContent).not.toContain('youtube_thumbnail_link_conversion_notice');
|
||||||
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ link: thumbnailLink });
|
||||||
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ content: `reply body ${youtubeLink}` });
|
||||||
|
} finally {
|
||||||
|
vi.clearAllTimers();
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects unresolved YouTube links on media-only reply modal boards', async () => {
|
it('rejects unresolved YouTube links on media-only reply modal boards', async () => {
|
||||||
const youtubeLink = 'https://youtu.be/replymissing';
|
const youtubeLink = 'https://youtu.be/replymissing';
|
||||||
testState.fetchMock.mockResolvedValue({
|
testState.fetchMock.mockResolvedValue({
|
||||||
@@ -1028,7 +1082,7 @@ describe('ReplyModal', () => {
|
|||||||
|
|
||||||
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
|
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
|
||||||
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
||||||
content: `${youtubeLink}\nreply body`,
|
content: `reply body ${youtubeLink}`,
|
||||||
link: thumbnailLink,
|
link: thumbnailLink,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -441,12 +441,11 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
setPublishReplyOptions({ link: nextUrl });
|
setPublishReplyOptions({ link: nextUrl });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConvertedContentChange = (content: string) => {
|
const handleConvertedContentChange = (content: string, selectionStart: number, selectionEnd: number) => {
|
||||||
const nextSelection = content.length;
|
|
||||||
if (textRef.current) {
|
if (textRef.current) {
|
||||||
textRef.current.setSelectionRange(nextSelection, nextSelection);
|
textRef.current.setSelectionRange(selectionStart, selectionEnd);
|
||||||
}
|
}
|
||||||
handleContentValueChange(content, nextSelection, nextSelection);
|
handleContentValueChange(content, selectionStart, selectionEnd);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -455,7 +454,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
noticeCountdown: youtubeThumbnailConversionCountdown,
|
noticeCountdown: youtubeThumbnailConversionCountdown,
|
||||||
queueLinkConversion,
|
queueLinkConversion,
|
||||||
} = useYouTubeThumbnailLinkConversion({
|
} = useYouTubeThumbnailLinkConversion({
|
||||||
enabled: requireReplyLinkIsMedia && !noReplyLinks,
|
enabled: !noReplyLinks,
|
||||||
onContentChange: handleConvertedContentChange,
|
onContentChange: handleConvertedContentChange,
|
||||||
onLinkChange: setLinkValue,
|
onLinkChange: setLinkValue,
|
||||||
textRef,
|
textRef,
|
||||||
@@ -665,7 +664,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
ref={urlRef}
|
ref={urlRef}
|
||||||
aria-label={requireReplyLinkIsMedia ? t('link_to_file') : t('link')}
|
aria-label={requireReplyLinkIsMedia ? t('link_to_file') : t('link')}
|
||||||
placeholder={requireReplyLinkIsMedia ? FILE_LINK_PLACEHOLDER : capitalize(t('link'))}
|
placeholder={requireReplyLinkIsMedia ? FILE_LINK_PLACEHOLDER : capitalize(t('link'))}
|
||||||
disabled={isUploading || noReplyLinks}
|
disabled={isUploading || youtubeThumbnailConversionCountdown !== null || noReplyLinks}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleLinkChange(e.target.value);
|
handleLinkChange(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -15,16 +15,32 @@ interface PendingYouTubeThumbnailLinkConversion {
|
|||||||
|
|
||||||
interface UseYouTubeThumbnailLinkConversionOptions {
|
interface UseYouTubeThumbnailLinkConversionOptions {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
onContentChange: (content: string) => void;
|
onContentChange: (content: string, selectionStart: number, selectionEnd: number) => void;
|
||||||
onLinkChange: (link: string) => void;
|
onLinkChange: (link: string) => void;
|
||||||
textRef: ElementRef<HTMLTextAreaElement>;
|
textRef: ElementRef<HTMLTextAreaElement>;
|
||||||
urlRef: ElementRef<HTMLInputElement>;
|
urlRef: ElementRef<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContentWithYouTubeLinkAtTop = (content: string, youtubeLink: string): string => {
|
const clampSelectionPosition = (position: number | null | undefined, contentLength: number): number => {
|
||||||
if (!content) return youtubeLink;
|
if (typeof position !== 'number' || Number.isNaN(position)) return contentLength;
|
||||||
if (content === youtubeLink || content.startsWith(`${youtubeLink}\n`) || content.startsWith(`${youtubeLink}\r\n`)) return content;
|
return Math.max(0, Math.min(position, contentLength));
|
||||||
return `${youtubeLink}\n${content}`;
|
};
|
||||||
|
|
||||||
|
const getContentWithInsertedYouTubeLink = (content: string, youtubeLink: string, selectionStart?: number | null, selectionEnd?: number | null) => {
|
||||||
|
const start = clampSelectionPosition(selectionStart, content.length);
|
||||||
|
const end = Math.max(start, clampSelectionPosition(selectionEnd, content.length));
|
||||||
|
const before = content.slice(0, start);
|
||||||
|
const after = content.slice(end);
|
||||||
|
const leadingSeparator = before && !/\s$/.test(before) ? ' ' : '';
|
||||||
|
const trailingSeparator = after && !/^\s/.test(after) ? ' ' : '';
|
||||||
|
const insertedText = `${leadingSeparator}${youtubeLink}${trailingSeparator}`;
|
||||||
|
const nextSelection = before.length + insertedText.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: `${before}${insertedText}${after}`,
|
||||||
|
selectionEnd: nextSelection,
|
||||||
|
selectionStart: nextSelection,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPendingConversion = (link: string): PendingYouTubeThumbnailLinkConversion | null => {
|
const getPendingConversion = (link: string): PendingYouTubeThumbnailLinkConversion | null => {
|
||||||
@@ -80,17 +96,19 @@ export const useYouTubeThumbnailLinkConversion = ({ enabled, onContentChange, on
|
|||||||
pendingConversionRef.current = null;
|
pendingConversionRef.current = null;
|
||||||
setNoticeCountdown(null);
|
setNoticeCountdown(null);
|
||||||
|
|
||||||
const currentContent = textRef.current?.value || '';
|
const textarea = textRef.current;
|
||||||
const nextContent = getContentWithYouTubeLinkAtTop(currentContent, pendingConversion.youtubeLink);
|
const currentContent = textarea?.value || '';
|
||||||
|
const nextContent = getContentWithInsertedYouTubeLink(currentContent, pendingConversion.youtubeLink, textarea?.selectionStart, textarea?.selectionEnd);
|
||||||
|
|
||||||
if (textRef.current) {
|
if (textarea) {
|
||||||
textRef.current.value = nextContent;
|
textarea.value = nextContent.content;
|
||||||
|
textarea.setSelectionRange(nextContent.selectionStart, nextContent.selectionEnd);
|
||||||
}
|
}
|
||||||
if (urlRef.current) {
|
if (urlRef.current) {
|
||||||
urlRef.current.value = thumbnailLink;
|
urlRef.current.value = thumbnailLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentChange(nextContent);
|
onContentChange(nextContent.content, nextContent.selectionStart, nextContent.selectionEnd);
|
||||||
onLinkChange(thumbnailLink);
|
onLinkChange(thumbnailLink);
|
||||||
return true;
|
return true;
|
||||||
}, [cancelPendingConversion, clearCountdownTimer, enabled, onContentChange, onLinkChange, textRef, urlRef]);
|
}, [cancelPendingConversion, clearCountdownTimer, enabled, onContentChange, onLinkChange, textRef, urlRef]);
|
||||||
|
|||||||
Reference in New Issue
Block a user