feat: use s.5chan.app share links and remove report menu items

Copy share links as path-based s.5chan.app URLs so the share server can render social previews, rename the menu label, remove the stub report action, and update the FAQ.
This commit is contained in:
Tommaso Casaburi
2026-06-06 19:06:04 +07:00
parent aa5a985201
commit 404a464310
42 changed files with 97 additions and 166 deletions
@@ -144,7 +144,7 @@ describe('PostMenuDesktop', () => {
await renderMenu();
await openMenu();
const copyLink = Array.from(document.body.querySelectorAll('button')).find((node) => node.textContent === 'copy_direct_link');
const copyLink = Array.from(document.body.querySelectorAll('button')).find((node) => node.textContent === 'copy_share_link');
await act(async () => {
copyLink?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
@@ -46,7 +46,7 @@ const CopyLinkButton = ({ cid, communityAddress, linkType, onClose }: CopyLinkBu
const { t } = useTranslation();
const directories = useDirectories();
const boardIdentifier = getBoardPath(communityAddress, directories);
const copyDirectLink = async () => {
const copyShareLink = async () => {
await safeCopyShareLink(boardIdentifier, linkType, linkType === 'thread' ? cid : undefined);
onClose();
};
@@ -55,15 +55,15 @@ const CopyLinkButton = ({ cid, communityAddress, linkType, onClose }: CopyLinkBu
type='button'
className={styles.postMenuItem}
tabIndex={0}
onClick={copyDirectLink}
onClick={copyShareLink}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
copyDirectLink();
copyShareLink();
}
}}
>
{t('copy_direct_link')}
{t('copy_share_link')}
</button>
);
};
@@ -225,24 +225,6 @@ const PostMenuDesktop = ({ postMenu }: PostMenuDesktopProps) => {
createPortal(
<FloatingFocusManager context={context} modal={false}>
<div className={styles.postMenu} ref={refs.setFloating} style={floatingStyles} aria-labelledby={headingId} {...getFloatingProps()}>
<button
type='button'
className={styles.postMenuItem}
tabIndex={0}
onClick={() => {
alert("Reporting isn't available yet.");
handleClose();
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
alert("Reporting isn't available yet.");
handleClose();
}
}}
>
{t('report_post')}
</button>
{!(isInPostPageView && postCid === cid) && (
<button
type='button'
@@ -185,7 +185,7 @@ describe('PostMenuMobile', () => {
expect(document.body.querySelector('[data-testid="edit-menu"]')?.textContent).toBe('cid-1');
await openMenu();
const copyLink = Array.from(document.body.querySelectorAll('div')).find((node) => node.textContent === 'copy_direct_link');
const copyLink = Array.from(document.body.querySelectorAll('div')).find((node) => node.textContent === 'copy_share_link');
await act(async () => {
copyLink?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
@@ -65,7 +65,7 @@ const CopyLinkButton = ({ cid, communityAddress, linkType, onClose }: CopyLinkBu
const { t } = useTranslation();
const directories = useDirectories();
const boardIdentifier = getBoardPath(communityAddress, directories);
const copyDirectLink = async () => {
const copyShareLink = async () => {
await copyShareLinkSafe(boardIdentifier, linkType, linkType === 'thread' ? cid : undefined);
onClose();
};
@@ -73,15 +73,15 @@ const CopyLinkButton = ({ cid, communityAddress, linkType, onClose }: CopyLinkBu
<button
type='button'
tabIndex={0}
onClick={copyDirectLink}
onClick={copyShareLink}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
copyDirectLink();
copyShareLink();
}
}}
>
<div className={styles.postMenuItem}>{t('copy_direct_link')}</div>
<div className={styles.postMenuItem}>{t('copy_share_link')}</div>
</button>
);
};
@@ -150,29 +150,6 @@ const ImageSearchButtons = ({ url, onClose }: { url: string; onClose: () => void
);
};
const ReportPostButton = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const reportPost = () => {
alert("Reporting isn't available yet.");
onClose();
};
return (
<button
type='button'
tabIndex={0}
onClick={reportPost}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
reportPost();
}
}}
>
<div className={styles.postMenuItem}>{t('report_post')}</div>
</button>
);
};
type DeletePostButtonProps = {
post: Comment;
onClose: () => void;
@@ -353,7 +330,6 @@ const PostMenuMobile = ({ postMenu, editMenuPost }: PostMenuMobileProps) => {
createPortal(
<FloatingFocusManager context={context} modal={false}>
<div className={styles.postMenu} ref={refs.setFloating} style={floatingStyles} aria-labelledby={headingId} {...getFloatingProps()}>
<ReportPostButton onClose={handleClose} />
{cid && communityAddress && <HidePostButton cid={cid} comment={postMenu.comment} isReply={!!parentCid} postCid={postCid} onClose={handleClose} />}
{(isAccountCommentAuthor || canAttemptAuthorDelete) && cid && editMenuPost && <DeletePostButton post={editMenuPost} onClose={handleClose} />}
{cid && communityAddress && <CopyLinkButton cid={cid} communityAddress={communityAddress} linkType='thread' onClose={handleClose} />}
+5 -3
View File
@@ -72,12 +72,12 @@ describe('url-utils', () => {
expect(getExpiringMediaLinkHostname('not-a-url')).toBeNull();
});
it('copies share links for threads and catalog pages using the production fallback base url', async () => {
it('copies path-based share links for threads and catalog pages using the share subdomain', async () => {
await copyShareLinkToClipboard('music.eth', 'thread', 'cid-123');
expect(testState.copyToClipboardMock).toHaveBeenCalledWith('https://5chan.app/#/music.eth/thread/cid-123');
expect(testState.copyToClipboardMock).toHaveBeenCalledWith('https://s.5chan.app/music.eth/thread/cid-123');
await copyShareLinkToClipboard('music.eth', 'catalog');
expect(testState.copyToClipboardMock).toHaveBeenCalledWith('https://5chan.app/#/music.eth/catalog');
expect(testState.copyToClipboardMock).toHaveBeenCalledWith('https://s.5chan.app/music.eth/catalog');
const copyThreadWithoutCid = copyShareLinkToClipboard as (boardIdentifier: string, linkType: 'thread', cid?: string) => Promise<void>;
await expect(copyThreadWithoutCid('music.eth', 'thread')).rejects.toThrow('copyShareLinkToClipboard: thread links require a cid');
@@ -89,6 +89,8 @@ describe('url-utils', () => {
expect(is5chanLink('https://5chan.app/#/music.eth/catalog')).toBe(true);
expect(is5chanLink('https://5chan.app/p/music.eth/c/cid-123')).toBe(true);
expect(is5chanLink('https://5chan.app/all/catalog')).toBe(true);
expect(is5chanLink('https://s.5chan.app/music.eth/thread/cid-123')).toBe(true);
expect(is5chanLink('https://s.5chan.app/music.eth/catalog')).toBe(true);
expect(is5chanLink('https://example.com/music.eth')).toBe(false);
});
+10 -4
View File
@@ -128,10 +128,14 @@ export const getPublishURLFilename = (url: string): string | null => {
}
};
const CHAN_5_HOSTNAMES = ['5chan.app', '5chan.eth.limo', '5chan.eth.link', '5chan.eth.sucks', '5chan.netlify.app'];
// Dedicated subdomain that serves shared posts. Its server renders social-media link
// previews (OpenGraph/Twitter cards) for the shared post before handing off to the app.
const SHARE_HOSTNAME = 's.5chan.app';
const CHAN_5_HOSTNAMES = ['5chan.app', SHARE_HOSTNAME, '5chan.eth.limo', '5chan.eth.link', '5chan.eth.sucks', '5chan.netlify.app'];
function getShareBaseUrl(): string {
return `https://${CHAN_5_HOSTNAMES[0]}`;
return `https://${SHARE_HOSTNAME}`;
}
export type ShareLinkType = 'thread' | 'catalog';
@@ -140,16 +144,18 @@ export type ShareLinkType = 'thread' | 'catalog';
export function copyShareLinkToClipboard(boardIdentifier: string, linkType: 'thread', cid: string): Promise<void>;
export function copyShareLinkToClipboard(boardIdentifier: string, linkType: Exclude<ShareLinkType, 'thread'>): Promise<void>;
export async function copyShareLinkToClipboard(boardIdentifier: string, linkType: ShareLinkType, cid?: string): Promise<void> {
// Share links are path-based (no HashRouter `#/`) so the share server can read the post
// from the request path and render its preview; URL fragments never reach the server.
if (linkType === 'thread') {
if (!cid) {
throw new Error('copyShareLinkToClipboard: thread links require a cid');
}
const shareLink = `${getShareBaseUrl()}/#/${boardIdentifier}/thread/${cid}`;
const shareLink = `${getShareBaseUrl()}/${boardIdentifier}/thread/${cid}`;
await copyToClipboard(shareLink);
return;
}
const shareLink = `${getShareBaseUrl()}/#/${boardIdentifier}/${linkType}`;
const shareLink = `${getShareBaseUrl()}/${boardIdentifier}/${linkType}`;
await copyToClipboard(shareLink);
}
+2 -2
View File
@@ -230,8 +230,8 @@ const FAQ_SECTIONS: FAQSection[] = [
question: 'How do I report posts?',
answer: (
<>
Reporting is not available yet. For now, board moderation happens through board-specific moderator tools and rules. If you are a board moderator, use the mod
queue and edit controls available for that board.
5chan does not use manual user reports. Moderation is handled automatically by AI moderation, and board moderators review flagged content through the mod
queue. If you run a board, use the mod queue and edit controls available for that board.
</>
),
},