From ee6ba45e5bf52ffe7cde35c7465944d9461e3a77 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 7 Jun 2026 06:59:52 +0200 Subject: [PATCH] make gif picker easier to use --- package.json | 2 +- src/components/GifPicker/index.tsx | 403 +++++++++++++++++------------ src/i18n/locales/en.ts | 7 + src/services/Untitled | 1 + 4 files changed, 242 insertions(+), 171 deletions(-) create mode 100644 src/services/Untitled diff --git a/package.json b/package.json index 1a962630..a8bef0d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.19.2", + "version": "23.20.0", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", diff --git a/src/components/GifPicker/index.tsx b/src/components/GifPicker/index.tsx index f2ea95ff..d50f8a18 100644 --- a/src/components/GifPicker/index.tsx +++ b/src/components/GifPicker/index.tsx @@ -8,6 +8,7 @@ import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { ScrollArea } from '@/components/ui/scroll-area' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Skeleton } from '@/components/ui/skeleton' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useUserReadInboxUrls } from '@/hooks/useUserMailboxRelayUrls' @@ -41,10 +42,12 @@ const EMPTY_FOLLOWING_PUBKEYS: readonly string[] = [] const GIFBUDDY_SEARCH_URL = (q: string) => q.trim() ? `${GIFBUDDY_URL}gifsearch?q=${encodeURIComponent(q.trim())}` : GIFBUDDY_URL -/** Lock drawer height at open so mobile keyboard / dvh changes do not resize the sheet. */ +type GifPickerTab = 'find' | 'import' + +/** Shorter sheet on mobile — tall drawers fight the post composer and keyboard. */ function mobileDrawerHeightPx(): number { const vh = window.visualViewport?.height ?? window.innerHeight - return Math.min(Math.round(vh * 0.88), Math.round(vh - 80)) + return Math.min(Math.round(vh * 0.6), Math.round(vh - 120)) } export default function GifPicker({ @@ -86,6 +89,7 @@ export default function GifPicker({ const gifbuddyPopupRef = useRef(null) const pickerRootRef = useRef(null) const [mobileDrawerHeight, setMobileDrawerHeight] = useState() + const [activeTab, setActiveTab] = useState('find') /** Keep drawer content mounted until Vaul's close animation finishes (avoids empty-sheet flicker). */ const [drawerContentMounted, setDrawerContentMounted] = useState(false) @@ -186,6 +190,13 @@ export default function GifPicker({ if (open) setDrawerContentMounted(true) }, [open]) + useEffect(() => { + if (!open) return + setActiveTab('find') + setSearchInput('') + applyLocalFilter('') + }, [open, applyLocalFilter]) + const preparePickerClose = useCallback(() => { loadGenerationRef.current += 1 setLoading(false) @@ -270,7 +281,7 @@ export default function GifPicker({ /** Open GifBuddy in a new tab (not a popup) so the picker doesn't close from focus loss. Listen for postMessage in case GifBuddy adds embed support. */ const openGifBuddySearch = useCallback(() => { - const url = GIFBUDDY_SEARCH_URL(searchInput) + const url = GIFBUDDY_SEARCH_URL(pasteUrl || searchInput) const w = window.open(url, '_blank', 'noopener,noreferrer') gifbuddyPopupRef.current = w ?? null const handler = (event: MessageEvent) => { @@ -293,7 +304,7 @@ export default function GifPicker({ gifbuddyPopupRef.current = null }, 10 * 60 * 1000) if (w) w.addEventListener('beforeunload', () => { clearTimeout(t); window.removeEventListener('message', handler) }) - }, [searchInput, onSelect, handleOpenChange]) + }, [pasteUrl, searchInput, onSelect, handleOpenChange]) const descriptionForPublish = publishDescription.trim() @@ -375,81 +386,188 @@ export default function GifPicker({ /** In drawer mode we constrain height and make only the GIF grid scroll so the drawer doesn't "sink" */ const isDrawer = isSmallScreen - const gifGrid = loading ? ( -
- {Array.from({ length: 8 }).map((_, i) => ( - - ))} -
- ) : ( -
- {gifs.map((gif) => { - const showArchive = gifShouldOfferNip94Archive(gif) && isLoggedIn - return ( -
- - + loading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : ( +
+ {items.map((gif) => { + const showArchive = showArchiveActions && gifShouldOfferNip94Archive(gif) && isLoggedIn + return ( +
- {gifSourceKindShortLabel(gif)} - - {showArchive && ( - - )} -
- ) - })} + { + const el = e.target as HTMLImageElement + const fallback = gif.fallbackUrl?.trim() + if (fallback && el.dataset.gifFallbackTried !== '1') { + el.dataset.gifFallbackTried = '1' + el.src = fallback + return + } + el.style.display = 'none' + }} + /> + + + {gifSourceKindShortLabel(gif)} + + {showArchive && ( + + )} +
+ ) + })} +
+ ) + + const scrollableGifGrid = (items: GifMetadata[], showArchiveActions: boolean) => + isDrawer ? ( +
+ {renderGifGrid(items, showArchiveActions)} +
+ ) : ( + + {renderGifGrid(items, showArchiveActions)} + + ) + + const importPanel = ( +
+

{t('Paste a GIF URL, upload your own file, or search GifBuddy.')}

+ +

+ {t('Opens GifBuddy in a new tab. Copy a GIF URL there, then paste it below.')} +

+
+ +
+ setPasteUrl(e.target.value)} + className="flex-1 min-w-0" + /> + +
+
+ {isLoggedIn && ( +
+ + setPublishDescription(e.target.value)} + className="min-w-0" + /> +
+ )} + {isLoggedIn && ( + <> + + + {uploadError &&

{uploadError}

} + + )} +
+ ) + + const findPanel = ( +
+

+ {t('Search your library and tap a GIF to insert.')} +

+ setSearchInput(e.target.value)} + className="shrink-0" + /> + {error &&

{error}

} + {scrollableGifGrid(gifs, !isDrawer)}
) - const content = ( + const tabbedContent = (
-
- setSearchInput(e.target.value)} - className="flex-1" - /> +
+

{t('Choose a GIF')}

- {error && ( -

{error}

- )} -
setActiveTab(v as GifPickerTab)} + className={cn('flex flex-col', isDrawer && 'min-h-0 flex-1')} > - {isDrawer ? ( -
- {gifGrid} -
- ) : ( - {gifGrid} - )} -
-
-
- -

- {t('Opens in a new tab. Copy a GIF URL there, then paste below. If this picker closed, click “Insert GIF” again to paste.')} -

-
- -
- setPasteUrl(e.target.value)} - className="flex-1 min-w-0" - /> - -
-
-
- {isLoggedIn && ( -
- - setPublishDescription(e.target.value)} - className="min-w-0" - /> -
- )} - {isLoggedIn && ( - <> - - - {uploadError && ( -

{uploadError}

- )} - - )} -
+ {t('Find GIF')} + + + {t('Import GIF')} + + + + {findPanel} + + + {importPanel} + +
) + const content = tabbedContent + if (isSmallScreen) { return ( { const t = e.target as HTMLElement | null diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 559aa7f5..91e50624 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -518,6 +518,13 @@ export default { 'Search GIFs': 'Search GIFs', 'Search memes': 'Search memes', 'Choose a GIF': 'Choose a GIF', + 'Find GIF': 'Find GIF', + 'Import GIF': 'Import GIF', + 'Search your library and tap a GIF to insert.': 'Search your library and tap a GIF to insert.', + 'Paste a GIF URL, upload your own file, or search GifBuddy.': + 'Paste a GIF URL, upload your own file, or search GifBuddy.', + 'Opens GifBuddy in a new tab. Copy a GIF URL there, then paste it below.': + 'Opens GifBuddy in a new tab. Copy a GIF URL there, then paste it below.', 'Choose a meme': 'Choose a meme', 'Search GifBuddy for more GIFs': 'Search GifBuddy for more GIFs', 'Add your own GIFs': 'Add your own GIFs', diff --git a/src/services/Untitled b/src/services/Untitled new file mode 100644 index 00000000..b4158c40 --- /dev/null +++ b/src/services/Untitled @@ -0,0 +1 @@ +I \ No newline at end of file