fix: copy GIF URLs as text to preserve animation

PNG conversion strips GIF animation. GIFs are now detected by URL
pattern (.gif extension or Tenor/Klipy CDN) and copied as URL text
instead, so pasting back into chat re-renders the animated GIF.
This commit is contained in:
Jannis Braun
2026-03-25 03:28:00 +01:00
parent aeb4ea6e1d
commit c255b0066a
2 changed files with 38 additions and 5 deletions
+24 -5
View File
@@ -27,21 +27,31 @@ export async function saveImage(url: string, filename?: string): Promise<void> {
/**
* Copies an image to the clipboard as PNG.
* GIFs are copied as URL text to preserve animation (PNG conversion strips it).
* Falls back to copying the URL as text if CORS or clipboard API blocks it.
*/
export async function copyImageToClipboard(url: string): Promise<void> {
// GIFs lose animation when converted to PNG — copy the URL instead
if (isGifUrl(url)) {
await navigator.clipboard.writeText(url);
useUIStore.getState().addToast('Copied GIF link', 'success', 3000);
return;
}
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const blob = await res.blob();
let pngBlob: Blob;
if (blob.type === 'image/png') {
pngBlob = blob;
} else {
pngBlob = await convertToPng(blob);
// If the server returned a GIF despite the URL not ending in .gif
if (blob.type === 'image/gif') {
await navigator.clipboard.writeText(url);
useUIStore.getState().addToast('Copied GIF link', 'success', 3000);
return;
}
const pngBlob = blob.type === 'image/png' ? blob : await convertToPng(blob);
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': pngBlob }),
]);
@@ -51,6 +61,15 @@ export async function copyImageToClipboard(url: string): Promise<void> {
}
}
/** Checks if a URL points to a GIF by extension or known GIF CDN patterns. */
function isGifUrl(url: string): boolean {
const path = url.split('?')[0]?.toLowerCase() ?? '';
if (path.endsWith('.gif')) return true;
// Tenor and Klipy serve GIFs even without .gif extension
if (/media\.tenor\.com|static\.klipy\.com/.test(url)) return true;
return false;
}
/** Draws a blob onto an offscreen canvas and exports as PNG. */
function convertToPng(blob: Blob): Promise<Blob> {
return new Promise((resolve, reject) => {