feat: support thumbnail for twitter and discord

This commit is contained in:
Shusui MOYATANI
2024-01-15 21:26:54 +09:00
parent 15add2eeee
commit 665393f867
5 changed files with 120 additions and 33 deletions

View File

@@ -46,7 +46,12 @@ const Post: Component<PostProps> = (props) => {
{(url) => (
<LazyLoad>
{() => (
<img src={thumbnailUrl(url)} alt="icon" class="h-full w-full object-cover" />
<img
src={thumbnailUrl(url, 'icon')}
alt="icon"
referrerpolicy="no-referrer"
class="h-full w-full object-cover"
/>
)}
</LazyLoad>
)}

View File

@@ -51,7 +51,7 @@ const ReactionDisplay: Component<ReactionDisplayProps> = (props) => {
<Show when={profile()?.picture} keyed>
{(url) => (
<img
src={thumbnailUrl(url)}
src={thumbnailUrl(url, 'icon')}
alt="icon"
// TODO autofit
class="h-full w-full object-cover"

View File

@@ -15,6 +15,7 @@ import lud06ToLnurlPayUrl from '@/nostr/zap/lud06ToLnurlPayUrl';
import lud16ToLnurlPayUrl from '@/nostr/zap/lud16ToLnurlPayUrl';
import ensureNonNull from '@/utils/ensureNonNull';
import { formatSiPrefix } from '@/utils/siPrefix';
import { thumbnailUrl } from '@/utils/url';
export type ZapReceiptProps = {
event: NostrEvent;
@@ -82,7 +83,7 @@ const ZapReceiptDisplay: Component<ZapReceiptProps> = (props) => {
<div class="author-icon h-5 w-5 shrink-0 overflow-hidden rounded">
<Show when={senderProfile()?.picture != null}>
<img
src={senderProfile()?.picture}
src={thumbnailUrl(senderProfile()?.picture, 'icon')}
alt="icon"
// TODO autofit
class="h-full w-full object-cover"

View File

@@ -5,9 +5,15 @@ import { describe, it } from 'vitest';
import { thumbnailUrl } from '@/utils/url';
describe('thumbnailUrl', () => {
it('should return an image url for a given imgur.com URL with additional path', () => {
const actual = thumbnailUrl('https://imgur.com/uBf5Qts.jpeg');
const expected = 'https://i.imgur.com/uBf5Qtsl.webp';
it('should return thumbnail url for imgur.com', () => {
const actual = thumbnailUrl('https://i.imgur.com/p05kUim.gif');
const expected = 'https://i.imgur.com/p05kUiml.gif';
assert.deepStrictEqual(actual, expected);
});
it('should return thumbnail url for imgur.com', () => {
const actual = thumbnailUrl('https://i.imgur.com/p05kUim.gif', 'icon');
const expected = 'https://i.imgur.com/p05kUims.gif';
assert.deepStrictEqual(actual, expected);
});
@@ -16,25 +22,41 @@ describe('thumbnailUrl', () => {
'https://nostr.build/i/2489ee648a4fef6943f4a7c88349477e78a91e28232246b801fe8ce86e64624e.png',
);
const expected =
'https://nostr.build/responsive/240p/i/2489ee648a4fef6943f4a7c88349477e78a91e28232246b801fe8ce86e64624e.png';
'https://image.nostr.build/resp/240p/2489ee648a4fef6943f4a7c88349477e78a91e28232246b801fe8ce86e64624e.png';
assert.deepStrictEqual(actual, expected);
});
it('should return url for image.nostr.build', () => {
const actual = thumbnailUrl(
'https://image.nostr.build/f56ee902307158c1ebbcb5ac00430dbf1425eac12d55e4277ebccbe54d09671b.jpg',
'https://image.nostr.build/78fc3c02f0488e2f3efb818adf1421bcee8c1612189e217c5ced1c2785eee1a8.jpg',
);
const expected =
'https://nostr.build/responsive/240p/i/f56ee902307158c1ebbcb5ac00430dbf1425eac12d55e4277ebccbe54d09671b.jpg';
'https://image.nostr.build/resp/240p/78fc3c02f0488e2f3efb818adf1421bcee8c1612189e217c5ced1c2785eee1a8.jpg';
assert.deepStrictEqual(actual, expected);
});
it('should return url for cdn.nostr.build', () => {
const actual = thumbnailUrl(
'https://cdn.nostr.build/i/6a2868ebb53da2c295e3a2a20a29fa009f230f721b71e88c7ffc3ec8eaae870f.png',
'https://cdn.nostr.build/i/78fc3c02f0488e2f3efb818adf1421bcee8c1612189e217c5ced1c2785eee1a8.jpg',
);
const expected =
'https://nostr.build/responsive/240p/i/6a2868ebb53da2c295e3a2a20a29fa009f230f721b71e88c7ffc3ec8eaae870f.png';
'https://image.nostr.build/resp/240p/78fc3c02f0488e2f3efb818adf1421bcee8c1612189e217c5ced1c2785eee1a8.jpg';
assert.deepStrictEqual(actual, expected);
});
it('should return url for pbs.twimg.com/profile_images/', () => {
const actual = thumbnailUrl(
'https://pbs.twimg.com/profile_images/1713367977725509632/iLgoXgtx_400x400.jpg',
);
const expected = 'https://pbs.twimg.com/profile_images/1713367977725509632/iLgoXgtx_normal.jpg';
assert.deepStrictEqual(actual, expected);
});
it('should return url for pbs.twimg.com/media/', () => {
const actual = thumbnailUrl(
'https://pbs.twimg.com/media/FPUltrpaAAQQdIO?format=png&name=900x900',
);
const expected = 'https://pbs.twimg.com/media/FPUltrpaAAQQdIO?format=jpg&name=small';
assert.deepStrictEqual(actual, expected);
});
});

View File

@@ -28,7 +28,10 @@ export const isWebSocketUrl = (urlString: string): boolean => {
/**
* Generate a URL of thumbnail for a given URL.
*/
export const thumbnailUrl = (urlString: string): string => {
export const thumbnailUrl = (
urlString: string,
size: 'icon' | 'thumbnail' = 'thumbnail',
): string => {
try {
const url = new URL(urlString);
// Imgur
@@ -37,8 +40,16 @@ export const thumbnailUrl = (urlString: string): string => {
if (match != null) {
const result = new URL(url);
const imageId = match[1];
const ext = match[2];
result.host = 'i.imgur.com';
result.pathname = `${imageId}l.webp`;
if (size === 'icon') {
result.pathname = `${imageId}s.${ext}`;
} else if (size === 'thumbnail') {
result.pathname = `${imageId}l.${ext}`;
} else {
// fallback
return urlString;
}
return result.toString();
}
return url.toString();
@@ -48,32 +59,80 @@ export const thumbnailUrl = (urlString: string): string => {
if (url.host === 'i.gyazo.com') {
const result = new URL(url);
result.host = 'thumb.gyazo.com';
if (size === 'icon') {
result.pathname = `/thumb/160${url.pathname}`;
} else if (size === 'thumbnail') {
result.pathname = `/thumb/640${url.pathname}`;
return result.toString();
}
// nostr.build
// https://github.com/nostrbuild/nostr.build/blob/main/api/v2/routes_upload.php
if (
url.host === 'nostr.build' ||
url.host === 'image.nostr.build' ||
url.host === 'cdn.nostr.build'
) {
const result = new URL(url);
result.host = 'nostr.build';
// profile pic (PFP)
if (url.pathname.startsWith('/i/p/')) return urlString;
if (url.pathname.startsWith('/i/')) {
result.pathname = `/responsive/240p${url.pathname}`;
} else if (url.pathname.match(/^\/[0-9a-zA-Z]+\.(jpeg|jpg|png|gif|webp|avif|apng)$/)) {
result.pathname = `/responsive/240p/i${url.pathname}`;
} else {
// fallback
return urlString;
}
return result.toString();
}
// nostr.build
// https://github.com/nostrbuild/nostr.build/blob/main/SiteConfig.php#L71-L75
// https://github.com/nostrbuild/nostr.build/blob/main/api/v2/routes_upload.php
if (
url.host === 'nostr.build' ||
url.host === 'i.nostr.build' ||
url.host === 'image.nostr.build' ||
url.host === 'cdn.nostr.build'
) {
// profile pic (PFP)
if (url.pathname.startsWith('/i/p/')) return urlString;
const result = new URL(url);
if (url.pathname.startsWith('/i/')) {
const withoutI = url.pathname.replace(/^\/i/, '');
result.hostname = 'image.nostr.build';
result.pathname = `/resp/240p${withoutI}`;
} else if (url.pathname.match(/^\/[0-9a-zA-Z]+\.(jpeg|jpg|png|gif|webp|avif|apng)$/)) {
result.pathname = `/resp/240p${url.pathname}`;
} else {
// fallback
return urlString;
}
return result.toString();
}
// pbs.twimg.com
// https://qiita.com/ma7ma7pipipi/items/713460b24710e0a46242
if (url.host === 'pbs.twimg.com') {
if (url.pathname.startsWith('/profile_images/')) {
const result = new URL(url);
result.pathname = url.pathname.replace(
/(?:_(?:mini|normal|bigger|200x200|400x400))?\.(jpg|png)$/,
'_normal.$1',
);
return result.toString();
}
if (url.pathname.startsWith('/media/')) {
const result = new URL(url);
result.searchParams.set('format', 'jpg');
result.searchParams.set('name', 'small');
return result.toString();
}
return urlString;
}
// media.discrodapp.net
if (
url.hostname === 'media.discordapp.net' &&
url.pathname.match(/^\/attachments\/\d+\/\d+\/[a-z0-9]+\.(png|jpg|gif)/)
) {
const result = new URL(url);
result.searchParams.set('format', 'webp');
if (size === 'icon') {
result.searchParams.set('width', '100');
result.searchParams.set('height', '100');
} else if (size === 'thumbnail') {
result.searchParams.set('width', '320');
result.searchParams.set('height', '320');
} else {
return urlString;
}
}
return url.toString();
} catch {
return urlString;