mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'codex/fix/link-file-filename-validation'
This commit is contained in:
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user