diff --git a/src/components/Post.tsx b/src/components/Post.tsx index b271fe6..62fd859 100644 --- a/src/components/Post.tsx +++ b/src/components/Post.tsx @@ -46,7 +46,12 @@ const Post: Component = (props) => { {(url) => ( {() => ( - icon + icon )} )} diff --git a/src/components/event/Reaction.tsx b/src/components/event/Reaction.tsx index 311ea24..6d1172c 100644 --- a/src/components/event/Reaction.tsx +++ b/src/components/event/Reaction.tsx @@ -51,7 +51,7 @@ const ReactionDisplay: Component = (props) => { {(url) => ( icon = (props) => {
icon { - 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); }); }); diff --git a/src/utils/url.ts b/src/utils/url.ts index de0040d..0233ba9 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -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'; - 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}`; + if (size === 'icon') { + result.pathname = `/thumb/160${url.pathname}`; + } else if (size === 'thumbnail') { + result.pathname = `/thumb/640${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;