mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(posting): clarify direct file links
This commit is contained in:
@@ -179,6 +179,7 @@ vi.mock('../../../hooks/use-file-upload', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/utils/media-utils', () => ({
|
||||
getDisplayMediaInfoType: (type: string, t: (key: string) => string) => t(type),
|
||||
getLinkMediaInfo: (link: string) => {
|
||||
if (link.endsWith('.gif')) {
|
||||
return { type: 'gif', url: link };
|
||||
@@ -186,6 +187,9 @@ vi.mock('../../../lib/utils/media-utils', () => ({
|
||||
if (link.endsWith('.png')) {
|
||||
return { type: 'image', url: link };
|
||||
}
|
||||
if (link.endsWith('.mp4')) {
|
||||
return { type: 'video', url: link };
|
||||
}
|
||||
return { type: 'link', url: link };
|
||||
},
|
||||
}));
|
||||
@@ -252,7 +256,9 @@ const clickByText = async (scope: ParentNode, text: string, index = 0) => {
|
||||
|
||||
const dispatchInput = async (element: HTMLInputElement | HTMLTextAreaElement, value: string) => {
|
||||
await act(async () => {
|
||||
element.value = value;
|
||||
const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
||||
descriptor?.set?.call(element, value);
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
@@ -379,6 +385,7 @@ describe('PostForm', () => {
|
||||
expect(nameInput).toBeTruthy();
|
||||
expect(subjectInput).toBeTruthy();
|
||||
expect(linkInput).toBeTruthy();
|
||||
expect(linkInput?.getAttribute('placeholder')).toBe('https://website.com/image.jpg');
|
||||
expect(textarea).toBeTruthy();
|
||||
expect(select).toBeTruthy();
|
||||
|
||||
@@ -409,6 +416,23 @@ describe('PostForm', () => {
|
||||
expect(testState.setPublishPostOptionsMock).toHaveBeenCalledWith({ communityAddress: 'music-posting.eth' });
|
||||
});
|
||||
|
||||
it('shows the pasted file-link filename 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[2];
|
||||
|
||||
expect(table?.textContent).toContain('no_file_chosen');
|
||||
|
||||
await dispatchInput(linkInput as HTMLInputElement, 'https://example.com/images/file%20name.jpg?size=large');
|
||||
|
||||
expect(table?.textContent).toContain('file name.jpg');
|
||||
});
|
||||
|
||||
it('redirects to the pending route when a post publish index is already available on mount', async () => {
|
||||
testState.postIndex = 7;
|
||||
testState.resolvedCommunityAddress = 'music-posting.eth';
|
||||
@@ -480,7 +504,12 @@ describe('LinkTypePreviewer', () => {
|
||||
await act(async () => {
|
||||
root.render(createElement(LinkTypePreviewer, { link: 'https://example.com/file.gif' }));
|
||||
});
|
||||
expect(container.textContent).toBe('animated_gif');
|
||||
expect(container.textContent).toBe('type: animated_gif');
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(LinkTypePreviewer, { link: 'https://example.com/file.mp4' }));
|
||||
});
|
||||
expect(container.textContent).toBe('type: video');
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(LinkTypePreviewer, { link: 'not-a-url' }));
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Comment, setAccount, useAccount, useEditedComment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import getShortAddress from '../../lib/get-short-address';
|
||||
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||
import { getLinkMediaInfo } from '../../lib/utils/media-utils';
|
||||
import { isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
|
||||
import { getDisplayMediaInfoType, getLinkMediaInfo } from '../../lib/utils/media-utils';
|
||||
import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
|
||||
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
||||
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
||||
@@ -25,6 +25,8 @@ import styles from './post-form.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const FILE_LINK_PLACEHOLDER = 'https://website.com/image.jpg';
|
||||
|
||||
export const LinkTypePreviewer = ({ link }: { link: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const mediaInfo = getLinkMediaInfo(link);
|
||||
@@ -35,9 +37,11 @@ export const LinkTypePreviewer = ({ link }: { link: string }) => {
|
||||
type = t('animated_gif');
|
||||
} else if (type === 'gif') {
|
||||
type = t('gif');
|
||||
} else if (type) {
|
||||
type = getDisplayMediaInfoType(type, t);
|
||||
}
|
||||
|
||||
return isValidURL(link) ? type : t('invalid_url');
|
||||
return isValidURL(link) ? `type: ${type}` : t('invalid_url');
|
||||
};
|
||||
|
||||
const PostFormActions = ({
|
||||
@@ -224,6 +228,7 @@ const PostFormFields = ({
|
||||
autoCorrect='off'
|
||||
autoComplete='off'
|
||||
spellCheck='false'
|
||||
placeholder={requirePostLinkIsMedia ? FILE_LINK_PLACEHOLDER : undefined}
|
||||
ref={urlRef}
|
||||
disabled={isUploading}
|
||||
onChange={(e) => {
|
||||
@@ -253,7 +258,7 @@ const PostFormFields = ({
|
||||
isUploading={isUploading}
|
||||
showUploadControls={showUploadControls}
|
||||
/>
|
||||
<span>{isUploading ? t('uploading') : uploadedFileName || t('no_file_chosen')}</span>
|
||||
<span>{isUploading ? t('uploading') : getPublishURLFilename(url) || uploadedFileName || t('no_file_chosen')}</span>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
@@ -398,6 +398,7 @@ describe('ReplyModal', () => {
|
||||
await dispatchInput(linkInput, 'https://example.com/file.png');
|
||||
await clickButtonByText('post');
|
||||
|
||||
expect(container.textContent).toContain('file.png');
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ link: 'https://example.com/file.png' });
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -419,6 +420,7 @@ describe('ReplyModal', () => {
|
||||
});
|
||||
|
||||
expect(linkInput?.value).toBe('https://cdn.example/uploaded.png');
|
||||
expect(container.textContent).toContain('uploaded.png');
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ link: 'https://cdn.example/uploaded.png' });
|
||||
|
||||
testState.replyIndex = 3;
|
||||
@@ -463,7 +465,7 @@ describe('ReplyModal', () => {
|
||||
await renderReplyModal('/all/thread/post-1');
|
||||
|
||||
const linkInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[1];
|
||||
expect(linkInput?.getAttribute('placeholder')).toContain('Link_to_file');
|
||||
expect(linkInput?.getAttribute('placeholder')).toBe('https://website.com/image.jpg');
|
||||
expect(container.textContent).not.toContain('warning');
|
||||
expect(container.textContent).not.toContain('Spoiler?');
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { setAccount, useAccount } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { isValidPublishURL } from '../../lib/utils/url-utils';
|
||||
import { getPublishURLFilename, isValidPublishURL } from '../../lib/utils/url-utils';
|
||||
import { isAllView, isModView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import useSelectedTextStore from '../../stores/use-selected-text-store';
|
||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||
@@ -20,6 +20,8 @@ import debounce from 'lodash/debounce';
|
||||
import { useSpring, animated } from '@react-spring/web';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
|
||||
const FILE_LINK_PLACEHOLDER = 'https://website.com/image.jpg';
|
||||
|
||||
interface ReplyModalProps {
|
||||
closeModal: () => void;
|
||||
showReplyModal: boolean;
|
||||
@@ -63,6 +65,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lengthError, setLengthError] = useState<string | null>(null);
|
||||
const [url, setUrl] = useState('');
|
||||
|
||||
const checkContentLengthRef = useRef(
|
||||
debounce((content: string, t: TFunction) => {
|
||||
@@ -270,6 +273,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const { isUploading, uploadedFileName, handleUpload } = useFileUpload({
|
||||
onUploadComplete: (uploadedUrl: string) => {
|
||||
if (uploadedUrl) {
|
||||
setUrl(uploadedUrl);
|
||||
if (urlRef.current) {
|
||||
urlRef.current.value = uploadedUrl;
|
||||
}
|
||||
@@ -279,6 +283,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 hasInitializedDisplayName = useRef(false);
|
||||
useEffect(() => {
|
||||
@@ -332,9 +337,12 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
type='text'
|
||||
ref={urlRef}
|
||||
aria-label={requirePostLinkIsMedia ? t('link_to_file') : t('link')}
|
||||
placeholder={capitalize(requirePostLinkIsMedia ? t('link_to_file') : t('link'))}
|
||||
placeholder={requirePostLinkIsMedia ? FILE_LINK_PLACEHOLDER : capitalize(t('link'))}
|
||||
disabled={isUploading}
|
||||
onChange={(e) => setPublishReplyOptions({ link: e.target.value })}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
setPublishReplyOptions({ link: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
@@ -365,8 +373,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
{t('choose_file')}
|
||||
</button>
|
||||
</span>
|
||||
<span className={styles.uploadFileName} title={uploadedFileName || t('no_file_chosen')}>
|
||||
{isUploading ? t('uploading') : uploadedFileName || t('no_file_chosen')}
|
||||
<span className={styles.uploadFileName} title={displayedFileName || t('no_file_chosen')}>
|
||||
{isUploading ? t('uploading') : displayedFileName || t('no_file_chosen')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -11,6 +11,7 @@ vi.mock('../clipboard-utils', () => ({
|
||||
import {
|
||||
copyShareLinkToClipboard,
|
||||
getHostname,
|
||||
getPublishURLFilename,
|
||||
is5chanLink,
|
||||
isPrivateNetworkHostname,
|
||||
isValidCrossboardPattern,
|
||||
@@ -57,6 +58,8 @@ describe('url-utils', () => {
|
||||
expect(isValidPublishURL('https://i.imgur.com/YpB7qfa.jpg')).toBe(true);
|
||||
expect(isValidPublishURL('ftp://example.com/file.jpg')).toBe(false);
|
||||
expect(isValidPublishURL('not-a-url')).toBe(false);
|
||||
expect(getPublishURLFilename('https://example.com/images/file%20name.jpg?size=large')).toBe('file name.jpg');
|
||||
expect(getPublishURLFilename('not-a-url')).toBeNull();
|
||||
});
|
||||
|
||||
it('copies share links for threads and catalog pages using the production fallback base url', async () => {
|
||||
|
||||
@@ -75,6 +75,24 @@ export const isValidPublishURL = (url: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getPublishURLFilename = (url: string): string | null => {
|
||||
if (!isValidPublishURL(url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(normalizePublishURL(url));
|
||||
const filename = parsedUrl.pathname.split('/').filter(Boolean).pop();
|
||||
if (!filename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(filename);
|
||||
} catch {
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
|
||||
const CHAN_5_HOSTNAMES = ['5chan.app', '5chan.eth.limo', '5chan.eth.link', '5chan.eth.sucks', '5chan.netlify.app'];
|
||||
|
||||
function getShareBaseUrl(): string {
|
||||
|
||||
Reference in New Issue
Block a user