mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(embed): restore youtube thumbnails and file-row labels (#1148)
* fix(comment-content): render reason text as comment content * fix(post-form): use native browser styling for flash tag select Exclude the flash tag dropdown from themed post-form select styling so it renders with the browser's default select appearance, matching the flag selector. * fix(p2p-stats): show peer flags for DNS6 relay hostnames Extract embedded IPv6 addresses from dns6 multiaddrs so geo lookup and country flags work for relay peers that publish IPv6 via DNS hostnames. * fix(flags): hide geolocation-only selectors on /int/ and /sp/ Country-only boards auto-publish geographic location flags without showing a flag dropdown, matching existing /bant/ behavior. * fix(embed): restore youtube thumbnails and file-row labels Restore thumbnail-first previews for YouTube embeds in post media and markdown hover. Desktop posts show File with the thumbnail image URL and a youtube video type label. * fix(embed): address youtube thumbnail review feedback Translate the youtube video label, handle mobile/music YouTube hosts as standard YouTube URLs, keep affected mocks current, and cap default Vitest workers to reduce local CPU spikes.
This commit is contained in:
@@ -17,30 +17,34 @@ describe('comment-flag-selection', () => {
|
||||
expect(hasCommentFlagsForDirectory({ features: { hasFlags: true }, title: '/pol/ - Politically Incorrect' })).toBe(true);
|
||||
});
|
||||
|
||||
it('uses geographic location as the default for country flag boards', () => {
|
||||
it('uses geographic location as the default for other flag boards', () => {
|
||||
expect(
|
||||
getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'int',
|
||||
directoryCode: 'fit',
|
||||
features: { hasFlags: true },
|
||||
title: '/int/ - International',
|
||||
title: '/fit/ - Fitness',
|
||||
}),
|
||||
).toEqual([{ label: 'Geographic Location', value: 'country:auto' }]);
|
||||
});
|
||||
|
||||
it('does not expose a flag selector on /bant/ but still publishes geographic location', () => {
|
||||
it.each([
|
||||
{ directoryCode: 'bant', title: '/bant/ - International/Random' },
|
||||
{ directoryCode: 'int', title: '/int/ - International' },
|
||||
{ directoryCode: 'sp', title: '/sp/ - Sports' },
|
||||
])('does not expose a flag selector on /$directoryCode/ but still publishes geographic location', ({ directoryCode, title }) => {
|
||||
expect(
|
||||
getCommentFlagOptionsForDirectory({
|
||||
directoryCode: 'bant',
|
||||
directoryCode,
|
||||
features: { hasFlags: true },
|
||||
title: '/bant/ - International/Random',
|
||||
title,
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
getCommentFlagPublishOptionsForDirectory({
|
||||
directoryCode: 'bant',
|
||||
directoryCode,
|
||||
features: { hasFlags: true },
|
||||
title: '/bant/ - International/Random',
|
||||
title,
|
||||
}),
|
||||
).toEqual({
|
||||
challengeRequest: {
|
||||
|
||||
@@ -37,6 +37,13 @@ describe('extractIpv6FromAddress', () => {
|
||||
expect(extractIpv6FromAddress('/ip6/2001:4860:4860::8888/tcp/4001/ws')).toBe('2001:4860:4860::8888');
|
||||
expect(extractIpFromAddress('/ip6/2001:4860:4860::8888/tcp/4001/ws')).toBe('2001:4860:4860::8888');
|
||||
});
|
||||
|
||||
it('extracts an IPv6 embedded with dashes in a DNS hostname', () => {
|
||||
const address = '/dns6/2a11-6100-0-5e9f--0.k51qzi5uqu5djg5pdoi9a98.example/tcp/443/wss/p2p/12D3KooWExample';
|
||||
|
||||
expect(extractIpv6FromAddress(address)).toBe('2a11:6100:0:5e9f::0');
|
||||
expect(extractIpFromAddress(address)).toBe('2a11:6100:0:5e9f::0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPrivateOrReservedIpv4', () => {
|
||||
@@ -261,6 +268,31 @@ describe('fetchPeerMapLocation', () => {
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('resolves a peer location from a DNS6 hostname with an embedded IPv6', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
cityName: 'Reykjavik',
|
||||
countryCode: 'IS',
|
||||
ipAddress: '2a11:6100:0:5e9f::0',
|
||||
latitude: 64.1466,
|
||||
longitude: -21.9426,
|
||||
}),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await expect(fetchPeerMapLocation('/dns6/2a11-6100-0-5e9f--0.k51qzi5uqu5djg5pdoi9a98.example/tcp/443/wss')).resolves.toMatchObject({
|
||||
countryCode: 'is',
|
||||
label: 'Reykjavik, IS',
|
||||
lat: 64.1466,
|
||||
lon: -21.9426,
|
||||
source: 'geoip',
|
||||
});
|
||||
expect(fetchMock.mock.calls[0][0]).toBe('https://free.freeipapi.com/api/json/2a11%3A6100%3A0%3A5e9f%3A%3A0');
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getApproximateCountryCode', () => {
|
||||
|
||||
@@ -38,7 +38,7 @@ const NO_FLAG_OPTION: CommentFlagSelectOption = {
|
||||
};
|
||||
|
||||
/** Boards that always publish geographic location without showing a flag selector. */
|
||||
const AUTO_GEOGRAPHIC_FLAG_DIRECTORY_CODES = new Set(['bant']);
|
||||
const AUTO_GEOGRAPHIC_FLAG_DIRECTORY_CODES = new Set(['bant', 'int', 'sp']);
|
||||
|
||||
const getDirectoryCode = (directory: Pick<DirectoryCommunity, 'directoryCode' | 'title'> | undefined): string | undefined => {
|
||||
const directoryCode = directory?.directoryCode?.trim().toLowerCase();
|
||||
|
||||
+11
-1
@@ -284,7 +284,17 @@ export const extractIpv4FromAddress = (address: string): string | null => {
|
||||
|
||||
export const extractIpv6FromAddress = (address: string): string | null => {
|
||||
const direct = /\/ip6\/([^/]+)/.exec(address);
|
||||
return direct ? direct[1] : null;
|
||||
if (direct) return direct[1];
|
||||
const dns = /\/dns6\/([^/]+)/i.exec(address);
|
||||
const firstLabel = dns?.[1]?.split('.')[0];
|
||||
if (!firstLabel || !firstLabel.includes('-') || !/^[0-9a-f-]+$/i.test(firstLabel)) return null;
|
||||
const candidate = firstLabel.replaceAll('-', ':').toLowerCase();
|
||||
try {
|
||||
new URL(`http://[${candidate}]/`);
|
||||
return candidate;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const parseOctets = (ip: string): number[] | null => {
|
||||
|
||||
@@ -20,9 +20,13 @@ vi.mock('@bitsocial/bitsocial-react-hooks/dist/lib/localforage-lru/index.js', ()
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/embed', () => ({
|
||||
canEmbed: (url: URL) => testState.canEmbedHosts.has(url.hostname),
|
||||
}));
|
||||
vi.mock('../../../components/embed/embed-utils', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../../components/embed/embed-utils')>();
|
||||
return {
|
||||
...actual,
|
||||
canEmbed: (url: URL) => testState.canEmbedHosts.has(url.hostname),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@capacitor/core', () => ({
|
||||
Capacitor: {
|
||||
@@ -33,7 +37,16 @@ vi.mock('@capacitor/core', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
import { fetchWebpageThumbnailIfNeeded, getCommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getLinkMediaInfo, getMediaDimensions } from '../media-utils';
|
||||
import {
|
||||
fetchWebpageThumbnailIfNeeded,
|
||||
getCommentMediaInfo,
|
||||
getDisplayMediaInfoType,
|
||||
getHasThumbnail,
|
||||
getLinkMediaInfo,
|
||||
getMediaDimensions,
|
||||
getPostMediaTypeLabel,
|
||||
getYouTubeEmbedPostMediaFileLink,
|
||||
} from '../media-utils';
|
||||
|
||||
const clearMemoizedCache = (fn: unknown) => {
|
||||
const memoized = fn as { clear?: () => void };
|
||||
@@ -102,6 +115,18 @@ describe('media-utils', () => {
|
||||
expect(getDisplayMediaInfoType('unknown', t)).toBe('translated:webpage');
|
||||
});
|
||||
|
||||
it('uses the youtube thumbnail url for post media file links and labels', () => {
|
||||
const mediaInfo = {
|
||||
patternThumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg',
|
||||
type: 'iframe',
|
||||
url: 'https://www.youtube.com/watch?v=abc123',
|
||||
};
|
||||
|
||||
expect(getYouTubeEmbedPostMediaFileLink(mediaInfo)).toBe('https://img.youtube.com/vi/abc123/0.jpg');
|
||||
expect(getPostMediaTypeLabel(mediaInfo, 'iframe', (key) => key)).toBe('youtube_video');
|
||||
expect(getYouTubeEmbedPostMediaFileLink({ type: 'iframe', url: 'https://streamable.com/clip123' })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('recognizes which media types expose thumbnails', () => {
|
||||
expect(getHasThumbnail(undefined, 'https://example.com/file.png')).toBe(false);
|
||||
expect(getHasThumbnail({ type: 'image', url: 'https://example.com/file.png' }, 'https://example.com/file.png')).toBe(true);
|
||||
@@ -156,6 +181,12 @@ describe('media-utils', () => {
|
||||
type: 'iframe',
|
||||
url: 'https://yt.example/watch?v=yt123',
|
||||
});
|
||||
testState.canEmbedHosts = new Set(['yewtu.be']);
|
||||
expect(getLinkMediaInfo('https://yewtu.be/invidious123')).toEqual({
|
||||
patternThumbnailUrl: 'https://img.youtube.com/vi/invidious123/0.jpg',
|
||||
type: 'iframe',
|
||||
url: 'https://yewtu.be/invidious123',
|
||||
});
|
||||
});
|
||||
|
||||
it('builds comment media info and strips thumbnails for blacklisted domains', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import localForageLru from '@bitsocial/bitsocial-react-hooks/dist/lib/localforage-lru/index.js';
|
||||
import { canEmbed } from '../../components/embed';
|
||||
import { canEmbed, getYouTubeVideoId } from '../../components/embed/embed-utils';
|
||||
import memoize from 'memoizee';
|
||||
import { isPrivateNetworkHostname, isValidURL, parseHttpUrl } from './url-utils';
|
||||
import { Capacitor, CapacitorHttp } from '@capacitor/core';
|
||||
@@ -40,6 +40,31 @@ export const getDisplayMediaInfoType = (type: string, t: Translate) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getYouTubeEmbedPostMediaFileLink = (commentMediaInfo: CommentMediaInfo | undefined): string | undefined => {
|
||||
if (!commentMediaInfo || commentMediaInfo.type !== 'iframe' || !commentMediaInfo.url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsedUrl = parseHttpUrl(commentMediaInfo.url);
|
||||
if (!parsedUrl || !getYouTubeVideoId(parsedUrl)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return commentMediaInfo.patternThumbnailUrl || getPatternThumbnailUrl(parsedUrl);
|
||||
};
|
||||
|
||||
export const getPostMediaTypeLabel = (commentMediaInfo: CommentMediaInfo | undefined, resolvedType: string | undefined, t: Translate): string => {
|
||||
if (getYouTubeEmbedPostMediaFileLink(commentMediaInfo)) {
|
||||
return t('youtube_video');
|
||||
}
|
||||
|
||||
if (!resolvedType) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getDisplayMediaInfoType(resolvedType, t);
|
||||
};
|
||||
|
||||
export const getHasThumbnail = memoize(
|
||||
(commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
|
||||
if (!link || !commentMediaInfo) return false;
|
||||
@@ -55,17 +80,6 @@ export const getHasThumbnail = memoize(
|
||||
{ max: 1000 },
|
||||
);
|
||||
|
||||
const getYouTubeVideoId = (url: URL): string | null => {
|
||||
if (url.host.includes('youtu.be')) {
|
||||
return url.pathname.slice(1);
|
||||
} else if (url.pathname.includes('/shorts/')) {
|
||||
return url.pathname.split('/shorts/')[1].split('/')[0];
|
||||
} else if (url.searchParams.has('v')) {
|
||||
return url.searchParams.get('v');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getPatternThumbnailUrl = (url: URL): string | undefined => {
|
||||
const videoId = getYouTubeVideoId(url);
|
||||
if (videoId) {
|
||||
|
||||
Reference in New Issue
Block a user