mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 14:34:25 +01:00
feat: support thumbnail for twitter and discord
This commit is contained in:
@@ -46,7 +46,12 @@ const Post: Component<PostProps> = (props) => {
|
|||||||
{(url) => (
|
{(url) => (
|
||||||
<LazyLoad>
|
<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>
|
</LazyLoad>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const ReactionDisplay: Component<ReactionDisplayProps> = (props) => {
|
|||||||
<Show when={profile()?.picture} keyed>
|
<Show when={profile()?.picture} keyed>
|
||||||
{(url) => (
|
{(url) => (
|
||||||
<img
|
<img
|
||||||
src={thumbnailUrl(url)}
|
src={thumbnailUrl(url, 'icon')}
|
||||||
alt="icon"
|
alt="icon"
|
||||||
// TODO autofit
|
// TODO autofit
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import lud06ToLnurlPayUrl from '@/nostr/zap/lud06ToLnurlPayUrl';
|
|||||||
import lud16ToLnurlPayUrl from '@/nostr/zap/lud16ToLnurlPayUrl';
|
import lud16ToLnurlPayUrl from '@/nostr/zap/lud16ToLnurlPayUrl';
|
||||||
import ensureNonNull from '@/utils/ensureNonNull';
|
import ensureNonNull from '@/utils/ensureNonNull';
|
||||||
import { formatSiPrefix } from '@/utils/siPrefix';
|
import { formatSiPrefix } from '@/utils/siPrefix';
|
||||||
|
import { thumbnailUrl } from '@/utils/url';
|
||||||
|
|
||||||
export type ZapReceiptProps = {
|
export type ZapReceiptProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
@@ -82,7 +83,7 @@ const ZapReceiptDisplay: Component<ZapReceiptProps> = (props) => {
|
|||||||
<div class="author-icon h-5 w-5 shrink-0 overflow-hidden rounded">
|
<div class="author-icon h-5 w-5 shrink-0 overflow-hidden rounded">
|
||||||
<Show when={senderProfile()?.picture != null}>
|
<Show when={senderProfile()?.picture != null}>
|
||||||
<img
|
<img
|
||||||
src={senderProfile()?.picture}
|
src={thumbnailUrl(senderProfile()?.picture, 'icon')}
|
||||||
alt="icon"
|
alt="icon"
|
||||||
// TODO autofit
|
// TODO autofit
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ import { describe, it } from 'vitest';
|
|||||||
import { thumbnailUrl } from '@/utils/url';
|
import { thumbnailUrl } from '@/utils/url';
|
||||||
|
|
||||||
describe('thumbnailUrl', () => {
|
describe('thumbnailUrl', () => {
|
||||||
it('should return an image url for a given imgur.com URL with additional path', () => {
|
it('should return thumbnail url for imgur.com', () => {
|
||||||
const actual = thumbnailUrl('https://imgur.com/uBf5Qts.jpeg');
|
const actual = thumbnailUrl('https://i.imgur.com/p05kUim.gif');
|
||||||
const expected = 'https://i.imgur.com/uBf5Qtsl.webp';
|
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);
|
assert.deepStrictEqual(actual, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -16,25 +22,41 @@ describe('thumbnailUrl', () => {
|
|||||||
'https://nostr.build/i/2489ee648a4fef6943f4a7c88349477e78a91e28232246b801fe8ce86e64624e.png',
|
'https://nostr.build/i/2489ee648a4fef6943f4a7c88349477e78a91e28232246b801fe8ce86e64624e.png',
|
||||||
);
|
);
|
||||||
const expected =
|
const expected =
|
||||||
'https://nostr.build/responsive/240p/i/2489ee648a4fef6943f4a7c88349477e78a91e28232246b801fe8ce86e64624e.png';
|
'https://image.nostr.build/resp/240p/2489ee648a4fef6943f4a7c88349477e78a91e28232246b801fe8ce86e64624e.png';
|
||||||
assert.deepStrictEqual(actual, expected);
|
assert.deepStrictEqual(actual, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return url for image.nostr.build', () => {
|
it('should return url for image.nostr.build', () => {
|
||||||
const actual = thumbnailUrl(
|
const actual = thumbnailUrl(
|
||||||
'https://image.nostr.build/f56ee902307158c1ebbcb5ac00430dbf1425eac12d55e4277ebccbe54d09671b.jpg',
|
'https://image.nostr.build/78fc3c02f0488e2f3efb818adf1421bcee8c1612189e217c5ced1c2785eee1a8.jpg',
|
||||||
);
|
);
|
||||||
const expected =
|
const expected =
|
||||||
'https://nostr.build/responsive/240p/i/f56ee902307158c1ebbcb5ac00430dbf1425eac12d55e4277ebccbe54d09671b.jpg';
|
'https://image.nostr.build/resp/240p/78fc3c02f0488e2f3efb818adf1421bcee8c1612189e217c5ced1c2785eee1a8.jpg';
|
||||||
assert.deepStrictEqual(actual, expected);
|
assert.deepStrictEqual(actual, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return url for cdn.nostr.build', () => {
|
it('should return url for cdn.nostr.build', () => {
|
||||||
const actual = thumbnailUrl(
|
const actual = thumbnailUrl(
|
||||||
'https://cdn.nostr.build/i/6a2868ebb53da2c295e3a2a20a29fa009f230f721b71e88c7ffc3ec8eaae870f.png',
|
'https://cdn.nostr.build/i/78fc3c02f0488e2f3efb818adf1421bcee8c1612189e217c5ced1c2785eee1a8.jpg',
|
||||||
);
|
);
|
||||||
const expected =
|
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);
|
assert.deepStrictEqual(actual, expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
103
src/utils/url.ts
103
src/utils/url.ts
@@ -28,7 +28,10 @@ export const isWebSocketUrl = (urlString: string): boolean => {
|
|||||||
/**
|
/**
|
||||||
* Generate a URL of thumbnail for a given URL.
|
* 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 {
|
try {
|
||||||
const url = new URL(urlString);
|
const url = new URL(urlString);
|
||||||
// Imgur
|
// Imgur
|
||||||
@@ -37,8 +40,16 @@ export const thumbnailUrl = (urlString: string): string => {
|
|||||||
if (match != null) {
|
if (match != null) {
|
||||||
const result = new URL(url);
|
const result = new URL(url);
|
||||||
const imageId = match[1];
|
const imageId = match[1];
|
||||||
|
const ext = match[2];
|
||||||
result.host = 'i.imgur.com';
|
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 result.toString();
|
||||||
}
|
}
|
||||||
return url.toString();
|
return url.toString();
|
||||||
@@ -48,32 +59,80 @@ export const thumbnailUrl = (urlString: string): string => {
|
|||||||
if (url.host === 'i.gyazo.com') {
|
if (url.host === 'i.gyazo.com') {
|
||||||
const result = new URL(url);
|
const result = new URL(url);
|
||||||
result.host = 'thumb.gyazo.com';
|
result.host = 'thumb.gyazo.com';
|
||||||
result.pathname = `/thumb/640${url.pathname}`;
|
if (size === 'icon') {
|
||||||
return result.toString();
|
result.pathname = `/thumb/160${url.pathname}`;
|
||||||
}
|
} else if (size === 'thumbnail') {
|
||||||
|
result.pathname = `/thumb/640${url.pathname}`;
|
||||||
// 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 {
|
} else {
|
||||||
|
// fallback
|
||||||
return urlString;
|
return urlString;
|
||||||
}
|
}
|
||||||
return result.toString();
|
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();
|
return url.toString();
|
||||||
} catch {
|
} catch {
|
||||||
return urlString;
|
return urlString;
|
||||||
|
|||||||
Reference in New Issue
Block a user