Merge branch 'codex/fix/link-file-filename-validation'

This commit is contained in:
Tommaso Casaburi
2026-06-09 12:34:18 +07:00
5 changed files with 79 additions and 28 deletions
@@ -309,7 +309,7 @@ vi.mock('../../../hooks/use-file-upload', () => ({
},
}));
vi.mock('../../loading-ellipsis', () => ({
vi.mock('../../loading-ellipsis/loading-ellipsis', () => ({
default: ({ string }: { string: string }) => createElement('span', { 'data-testid': 'loading-ellipsis' }, string),
}));
@@ -331,19 +331,26 @@ vi.mock('../../../lib/utils/media-utils', () => {
return {
getDisplayMediaInfoType: (type: string, t: (key: string) => string) => t(type),
getLinkMediaInfo: (link: string) => {
const path = (() => {
try {
return new URL(link).pathname;
} catch {
return link;
}
})();
if (getTwimgMediaFilePublishUrl(link)) {
return { type: 'image', url: link };
}
if (link.endsWith('.gif')) {
if (path.endsWith('.gif')) {
return { type: 'gif', url: link };
}
if (link.endsWith('.jpg') || link.endsWith('.jpeg') || link.endsWith('.png')) {
if (path.endsWith('.jpg') || path.endsWith('.jpeg') || path.endsWith('.png')) {
return { type: 'image', url: link };
}
if (link.endsWith('.mp4')) {
if (path.endsWith('.mp4')) {
return { type: 'video', url: link };
}
if (link.endsWith('.mp3')) {
if (path.endsWith('.mp3')) {
return { type: 'audio', url: link };
}
if (link.includes('youtube.com')) {
@@ -1306,6 +1313,23 @@ describe('PostForm', () => {
expect(table?.textContent).toContain('file name.jpg');
});
it('does not show a pasted non-file path segment next to the upload button', async () => {
testState.uploadedFileName = null;
await renderPostForm('/all');
await clickByText(container, 'start_new_thread');
const table = container.querySelector('table');
const textInputs = table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || [];
const linkInput = textInputs[3];
await dispatchInput(linkInput as HTMLInputElement, 'https://imgur.com/8EJ2T76');
expect(table?.textContent).toContain('not_a_file');
expect(table?.textContent).toContain('no_file_chosen');
expect(table?.textContent).not.toContain('8EJ2T76');
});
it('shows the rules and FAQ prompt at the bottom of the form with a board-specific rules link', async () => {
testState.resolvedCommunityAddress = 'music-posting.eth';
+11 -16
View File
@@ -6,7 +6,7 @@ import { Comment, setAccount, useAccount, useEditedComment } from '@bitsocial/bi
import getShortAddress from '../../lib/get-short-address';
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
import { getDisplayMediaInfoType, getLinkMediaInfo, getTwimgMediaFilePublishUrl } from '../../lib/utils/media-utils';
import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation-utils';
import { getExpiringMediaLinkAlert, getPublishFileDisplayName, isPublishFileMediaLink, isPublishFileMediaType } from '../../lib/utils/media-link-validation-utils';
import {
type DiceRoll,
type FortuneEntry,
@@ -21,7 +21,7 @@ import {
isPostOptionsValidationError,
} from '../../lib/utils/post-options-utils';
import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
import { isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
import { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils';
import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
import { getBoardPath } from '../../lib/utils/route-utils';
@@ -46,8 +46,8 @@ 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 LoadingEllipsis from '../loading-ellipsis/loading-ellipsis';
import OekakiDrawingControls from '../oekaki-drawing-controls/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';
@@ -55,23 +55,18 @@ import debounce from 'lodash/debounce';
const FILE_LINK_PLACEHOLDER = 'https://website.com/image.jpg';
const POST_FORM_FILE_DISPLAY_MAX_LENGTH = 28;
const POST_FORM_FILE_MEDIA_TYPES = new Set(['gif', 'image', 'video']);
const mergeFlairs = (...flairGroups: Array<Comment['flairs'] | undefined>): Comment['flairs'] | undefined => {
const flairs = flairGroups.flatMap((group) => (Array.isArray(group) ? group : []));
return flairs.length > 0 ? flairs : undefined;
};
const getPostFormFileDisplayLabel = (url: string, uploadedFileName: string | null | undefined, noFileLabel: string): string => {
const raw = getPublishURLFilename(url) || uploadedFileName;
const getPostFormFileDisplayLabel = (url: string, uploadedFileName: string | null | undefined, noFileLabel: string, requireFile: boolean): string => {
const raw = getPublishFileDisplayName(url, uploadedFileName, requireFile);
if (!raw) return noFileLabel;
return truncateWithEllipsisInMiddle(raw, POST_FORM_FILE_DISPLAY_MAX_LENGTH);
};
const isPostFormFileMediaType = (type: string | undefined): boolean => Boolean(type && POST_FORM_FILE_MEDIA_TYPES.has(type));
const isPostFormFileMediaLink = (link: string): boolean => isPostFormFileMediaType(getLinkMediaInfo(link)?.type);
const getPublishLinkOptions = (link: string, includeCurrentLink: boolean): Partial<Pick<Comment, 'link'>> => {
const twimgPublishUrl = getTwimgMediaFilePublishUrl(link);
if (twimgPublishUrl) {
@@ -87,7 +82,7 @@ export const LinkTypePreviewer = ({ link, requireFile = false }: { link: string;
let type = mediaInfo?.type;
const { status: gifFrameStatus } = useFetchGifFirstFrame(type === 'gif' ? mediaInfo?.url : undefined);
if (requireFile && isValidURL(link) && !isPostFormFileMediaType(type)) {
if (requireFile && isValidURL(link) && !isPublishFileMediaType(type)) {
return <span className={styles.linkTypeError}>{t('not_a_file')}</span>;
}
@@ -400,8 +395,8 @@ const PostFormFields = ({
isUploading={isUploading}
showUploadControls={showUploadControls}
/>
<span title={getPublishURLFilename(url) || uploadedFileName || undefined}>
{isUploading ? <LoadingEllipsis string={t('uploading')} /> : getPostFormFileDisplayLabel(url, uploadedFileName, t('no_file_chosen'))}
<span title={getPublishFileDisplayName(url, uploadedFileName, requirePostLinkIsMedia) || undefined}>
{isUploading ? <LoadingEllipsis string={t('uploading')} /> : getPostFormFileDisplayLabel(url, uploadedFileName, t('no_file_chosen'), requirePostLinkIsMedia)}
</span>
</td>
</tr>
@@ -653,7 +648,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
setFormError(`${t('error')}: ${t('invalid_url_alert')}`);
return;
}
if (currentUrl && requirePostLinkIsMedia && !isPostFormFileMediaLink(currentUrl)) {
if (currentUrl && requirePostLinkIsMedia && !isPublishFileMediaLink(currentUrl)) {
setFormError(`${t('error')}: ${t('link_not_image_or_video_alert')}`);
return;
}
@@ -781,7 +776,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
setFormError(`${t('error')}: ${t('invalid_url_alert')}`);
return;
}
if (currentUrl && requirePostLinkIsMedia && !isPostFormFileMediaLink(currentUrl)) {
if (currentUrl && requirePostLinkIsMedia && !isPublishFileMediaLink(currentUrl)) {
setFormError(`${t('error')}: ${t('link_not_image_or_video_alert')}`);
return;
}
@@ -223,7 +223,7 @@ vi.mock('../../../hooks/use-file-upload', () => ({
},
}));
vi.mock('../../loading-ellipsis', () => ({
vi.mock('../../loading-ellipsis/loading-ellipsis', () => ({
default: ({ string }: { string: string }) => createElement('span', { 'data-testid': 'loading-ellipsis' }, string),
}));
@@ -1021,6 +1021,21 @@ describe('ReplyModal', () => {
expect(container.textContent).not.toContain('Spoiler?');
});
it('only shows pasted link filenames after media-only links are confirmed as files', async () => {
await renderReplyModal('/all/thread/post-1');
const linkInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[2];
await dispatchInput(linkInput, 'https://imgur.com/8EJ2T76');
expect(container.textContent).toContain('no_file_chosen');
expect(container.textContent).not.toContain('8EJ2T76');
await dispatchInput(linkInput, 'https://imgur.com/8EJ2T76.jpg');
expect(container.textContent).toContain('8EJ2T76.jpg');
});
it('positions the draggable modal with left/top styles instead of a transform layer', async () => {
await renderReplyModal('/mu/thread/post-1');
+5 -5
View File
@@ -3,7 +3,7 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { setAccount, useAccount } from '@bitsocial/bitsocial-react-hooks';
import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation-utils';
import { getExpiringMediaLinkAlert, getPublishFileDisplayName } from '../../lib/utils/media-link-validation-utils';
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsForDirectory } from '../../lib/comment-flag-selection';
import {
type DiceRoll,
@@ -17,7 +17,7 @@ import {
hasNonokoOption,
isPostOptionsValidationError,
} from '../../lib/utils/post-options-utils';
import { getPublishURLFilename, isValidPublishURL } from '../../lib/utils/url-utils';
import { isValidPublishURL } from '../../lib/utils/url-utils';
import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
import { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils';
import { isAllView, isModView, isSubscriptionsView } from '../../lib/utils/view-utils';
@@ -35,8 +35,8 @@ 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 LoadingEllipsis from '../loading-ellipsis/loading-ellipsis';
import OekakiDrawingControls from '../oekaki-drawing-controls/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';
@@ -498,7 +498,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
};
const uploadMode = useMediaHostingStore((state) => state.uploadMode);
const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime());
const displayedFileName = getPublishURLFilename(url) || uploadedFileName;
const displayedFileName = getPublishFileDisplayName(url, uploadedFileName, requirePostLinkIsMedia);
const youtubeThumbnailConversionNotice =
youtubeThumbnailConversionCountdown !== null ? t('youtube_thumbnail_link_conversion_notice', { count: youtubeThumbnailConversionCountdown }) : null;
+18 -1
View File
@@ -1,8 +1,25 @@
import { getExpiringMediaLinkHostname } from './url-utils';
import { getExpiringMediaLinkHostname, getPublishURLFilename } from './url-utils';
import { getLinkMediaInfo } from './media-utils';
type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
const PUBLISH_FILE_MEDIA_TYPES = new Set(['gif', 'image', 'video']);
export const getExpiringMediaLinkAlert = (url: string, t: TranslateFn): string | null => {
const expiringMediaLinkHostname = getExpiringMediaLinkHostname(url);
return expiringMediaLinkHostname ? `${t('error')}: ${t('expiring_media_link_alert', { domain: expiringMediaLinkHostname })}` : null;
};
export const isPublishFileMediaType = (type: string | undefined): boolean => Boolean(type && PUBLISH_FILE_MEDIA_TYPES.has(type));
export const isPublishFileMediaLink = (link: string): boolean => isPublishFileMediaType(getLinkMediaInfo(link)?.type);
export const getPublishFileDisplayName = (url: string, uploadedFileName: string | null | undefined, requireFile: boolean): string | null => {
if (!url) {
return uploadedFileName || null;
}
if (requireFile && !isPublishFileMediaLink(url)) {
return null;
}
return getPublishURLFilename(url) || uploadedFileName || null;
};