fix(post-form): hide filename for non-file media links

Only show pasted link filenames when the URL resolves to publishable
file media, so bare paths like imgur album IDs are not shown as files.
This commit is contained in:
Tommaso Casaburi
2026-06-09 12:34:13 +07:00
parent 01e83411b0
commit 27558ded18
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), default: ({ string }: { string: string }) => createElement('span', { 'data-testid': 'loading-ellipsis' }, string),
})); }));
@@ -331,19 +331,26 @@ vi.mock('../../../lib/utils/media-utils', () => {
return { return {
getDisplayMediaInfoType: (type: string, t: (key: string) => string) => t(type), getDisplayMediaInfoType: (type: string, t: (key: string) => string) => t(type),
getLinkMediaInfo: (link: string) => { getLinkMediaInfo: (link: string) => {
const path = (() => {
try {
return new URL(link).pathname;
} catch {
return link;
}
})();
if (getTwimgMediaFilePublishUrl(link)) { if (getTwimgMediaFilePublishUrl(link)) {
return { type: 'image', url: link }; return { type: 'image', url: link };
} }
if (link.endsWith('.gif')) { if (path.endsWith('.gif')) {
return { type: 'gif', url: link }; 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 }; return { type: 'image', url: link };
} }
if (link.endsWith('.mp4')) { if (path.endsWith('.mp4')) {
return { type: 'video', url: link }; return { type: 'video', url: link };
} }
if (link.endsWith('.mp3')) { if (path.endsWith('.mp3')) {
return { type: 'audio', url: link }; return { type: 'audio', url: link };
} }
if (link.includes('youtube.com')) { if (link.includes('youtube.com')) {
@@ -1306,6 +1313,23 @@ describe('PostForm', () => {
expect(table?.textContent).toContain('file name.jpg'); 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 () => { 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'; 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 getShortAddress from '../../lib/get-short-address';
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages'; import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
import { getDisplayMediaInfoType, getLinkMediaInfo, getTwimgMediaFilePublishUrl } from '../../lib/utils/media-utils'; 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 { import {
type DiceRoll, type DiceRoll,
type FortuneEntry, type FortuneEntry,
@@ -21,7 +21,7 @@ import {
isPostOptionsValidationError, isPostOptionsValidationError,
} from '../../lib/utils/post-options-utils'; } from '../../lib/utils/post-options-utils';
import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-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 { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils';
import { hasModQueueAccessRole } from '../../lib/utils/mod-access'; import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
import { getBoardPath } from '../../lib/utils/route-utils'; 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 useMediaHostingStore from '../../stores/use-media-hosting-store';
import BoardOfflineAlert from '../board-offline-alert/board-offline-alert'; import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar'; import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
import LoadingEllipsis from '../loading-ellipsis'; import LoadingEllipsis from '../loading-ellipsis/loading-ellipsis';
import OekakiDrawingControls from '../oekaki-drawing-controls'; import OekakiDrawingControls from '../oekaki-drawing-controls/oekaki-drawing-controls';
import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message'; import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message';
import styles from './post-form.module.css'; import styles from './post-form.module.css';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
@@ -55,23 +55,18 @@ import debounce from 'lodash/debounce';
const FILE_LINK_PLACEHOLDER = 'https://website.com/image.jpg'; const FILE_LINK_PLACEHOLDER = 'https://website.com/image.jpg';
const POST_FORM_FILE_DISPLAY_MAX_LENGTH = 28; 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 mergeFlairs = (...flairGroups: Array<Comment['flairs'] | undefined>): Comment['flairs'] | undefined => {
const flairs = flairGroups.flatMap((group) => (Array.isArray(group) ? group : [])); const flairs = flairGroups.flatMap((group) => (Array.isArray(group) ? group : []));
return flairs.length > 0 ? flairs : undefined; return flairs.length > 0 ? flairs : undefined;
}; };
const getPostFormFileDisplayLabel = (url: string, uploadedFileName: string | null | undefined, noFileLabel: string): string => { const getPostFormFileDisplayLabel = (url: string, uploadedFileName: string | null | undefined, noFileLabel: string, requireFile: boolean): string => {
const raw = getPublishURLFilename(url) || uploadedFileName; const raw = getPublishFileDisplayName(url, uploadedFileName, requireFile);
if (!raw) return noFileLabel; if (!raw) return noFileLabel;
return truncateWithEllipsisInMiddle(raw, POST_FORM_FILE_DISPLAY_MAX_LENGTH); 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 getPublishLinkOptions = (link: string, includeCurrentLink: boolean): Partial<Pick<Comment, 'link'>> => {
const twimgPublishUrl = getTwimgMediaFilePublishUrl(link); const twimgPublishUrl = getTwimgMediaFilePublishUrl(link);
if (twimgPublishUrl) { if (twimgPublishUrl) {
@@ -87,7 +82,7 @@ export const LinkTypePreviewer = ({ link, requireFile = false }: { link: string;
let type = mediaInfo?.type; let type = mediaInfo?.type;
const { status: gifFrameStatus } = useFetchGifFirstFrame(type === 'gif' ? mediaInfo?.url : undefined); 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>; return <span className={styles.linkTypeError}>{t('not_a_file')}</span>;
} }
@@ -400,8 +395,8 @@ const PostFormFields = ({
isUploading={isUploading} isUploading={isUploading}
showUploadControls={showUploadControls} showUploadControls={showUploadControls}
/> />
<span title={getPublishURLFilename(url) || uploadedFileName || undefined}> <span title={getPublishFileDisplayName(url, uploadedFileName, requirePostLinkIsMedia) || undefined}>
{isUploading ? <LoadingEllipsis string={t('uploading')} /> : getPostFormFileDisplayLabel(url, uploadedFileName, t('no_file_chosen'))} {isUploading ? <LoadingEllipsis string={t('uploading')} /> : getPostFormFileDisplayLabel(url, uploadedFileName, t('no_file_chosen'), requirePostLinkIsMedia)}
</span> </span>
</td> </td>
</tr> </tr>
@@ -653,7 +648,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
setFormError(`${t('error')}: ${t('invalid_url_alert')}`); setFormError(`${t('error')}: ${t('invalid_url_alert')}`);
return; return;
} }
if (currentUrl && requirePostLinkIsMedia && !isPostFormFileMediaLink(currentUrl)) { if (currentUrl && requirePostLinkIsMedia && !isPublishFileMediaLink(currentUrl)) {
setFormError(`${t('error')}: ${t('link_not_image_or_video_alert')}`); setFormError(`${t('error')}: ${t('link_not_image_or_video_alert')}`);
return; return;
} }
@@ -781,7 +776,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
setFormError(`${t('error')}: ${t('invalid_url_alert')}`); setFormError(`${t('error')}: ${t('invalid_url_alert')}`);
return; return;
} }
if (currentUrl && requirePostLinkIsMedia && !isPostFormFileMediaLink(currentUrl)) { if (currentUrl && requirePostLinkIsMedia && !isPublishFileMediaLink(currentUrl)) {
setFormError(`${t('error')}: ${t('link_not_image_or_video_alert')}`); setFormError(`${t('error')}: ${t('link_not_image_or_video_alert')}`);
return; 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), default: ({ string }: { string: string }) => createElement('span', { 'data-testid': 'loading-ellipsis' }, string),
})); }));
@@ -1021,6 +1021,21 @@ describe('ReplyModal', () => {
expect(container.textContent).not.toContain('Spoiler?'); 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 () => { it('positions the draggable modal with left/top styles instead of a transform layer', async () => {
await renderReplyModal('/mu/thread/post-1'); 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 { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next'; import type { TFunction } from 'i18next';
import { setAccount, useAccount } from '@bitsocial/bitsocial-react-hooks'; 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 { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsForDirectory } from '../../lib/comment-flag-selection';
import { import {
type DiceRoll, type DiceRoll,
@@ -17,7 +17,7 @@ import {
hasNonokoOption, hasNonokoOption,
isPostOptionsValidationError, isPostOptionsValidationError,
} from '../../lib/utils/post-options-utils'; } 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 { hasModQueueAccessRole } from '../../lib/utils/mod-access';
import { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils'; import { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils';
import { isAllView, isModView, isSubscriptionsView } from '../../lib/utils/view-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 { OEKAKI_WEB_WARNING_TEXT } from '../../lib/oekaki/oekaki-copy';
import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar'; import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
import BoardOfflineAlert from '../board-offline-alert/board-offline-alert'; import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
import LoadingEllipsis from '../loading-ellipsis'; import LoadingEllipsis from '../loading-ellipsis/loading-ellipsis';
import OekakiDrawingControls from '../oekaki-drawing-controls'; import OekakiDrawingControls from '../oekaki-drawing-controls/oekaki-drawing-controls';
import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message'; import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message';
import styles from './reply-modal.module.css'; import styles from './reply-modal.module.css';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
@@ -498,7 +498,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
}; };
const uploadMode = useMediaHostingStore((state) => state.uploadMode); const uploadMode = useMediaHostingStore((state) => state.uploadMode);
const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime()); const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime());
const displayedFileName = getPublishURLFilename(url) || uploadedFileName; const displayedFileName = getPublishFileDisplayName(url, uploadedFileName, requirePostLinkIsMedia);
const youtubeThumbnailConversionNotice = const youtubeThumbnailConversionNotice =
youtubeThumbnailConversionCountdown !== null ? t('youtube_thumbnail_link_conversion_notice', { count: youtubeThumbnailConversionCountdown }) : null; 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; 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 => { export const getExpiringMediaLinkAlert = (url: string, t: TranslateFn): string | null => {
const expiringMediaLinkHostname = getExpiringMediaLinkHostname(url); const expiringMediaLinkHostname = getExpiringMediaLinkHostname(url);
return expiringMediaLinkHostname ? `${t('error')}: ${t('expiring_media_link_alert', { domain: expiringMediaLinkHostname })}` : null; 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;
};