diff --git a/package-lock.json b/package-lock.json index fcc5138e..d39a0864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.18.0", + "version": "23.18.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.18.0", + "version": "23.18.1", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index 4b5bcc2d..a5f6ac26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.18.0", + "version": "23.18.1", "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 7e2ed46d..f2ea95ff 100644 --- a/src/components/GifPicker/index.tsx +++ b/src/components/GifPicker/index.tsx @@ -41,6 +41,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. */ +function mobileDrawerHeightPx(): number { + const vh = window.visualViewport?.height ?? window.innerHeight + return Math.min(Math.round(vh * 0.88), Math.round(vh - 80)) +} + export default function GifPicker({ children, onSelect, @@ -78,6 +84,10 @@ export default function GifPicker({ const [publishDescription, setPublishDescription] = useState('') const fileInputRef = useRef(null) const gifbuddyPopupRef = useRef(null) + const pickerRootRef = useRef(null) + const [mobileDrawerHeight, setMobileDrawerHeight] = useState() + /** Keep drawer content mounted until Vaul's close animation finishes (avoids empty-sheet flicker). */ + const [drawerContentMounted, setDrawerContentMounted] = useState(false) const userReadRelays = useUserReadInboxUrls() @@ -167,13 +177,46 @@ export default function GifPicker({ void loadGifs() }, [open, loadGifs]) + useEffect(() => { + if (!open || !isSmallScreen) return + setMobileDrawerHeight(mobileDrawerHeightPx()) + }, [open, isSmallScreen]) + + useEffect(() => { + if (open) setDrawerContentMounted(true) + }, [open]) + + const preparePickerClose = useCallback(() => { + loadGenerationRef.current += 1 + setLoading(false) + const el = document.activeElement + if (el instanceof HTMLElement && pickerRootRef.current?.contains(el)) { + el.blur() + } + }, []) + + const handleOpenChange = useCallback( + (next: boolean) => { + if (!next) preparePickerClose() + setOpen(next) + }, + [preparePickerClose] + ) + + const handleDrawerAnimationEnd = useCallback((isOpen: boolean) => { + if (!isOpen) { + setDrawerContentMounted(false) + setMobileDrawerHeight(undefined) + } + }, []) + const handleSelect = useCallback( (gif: GifMetadata) => { const url = (gif.fallbackUrl?.trim() || gif.url).trim() if (!url) return const desc = publishDescription.trim() onSelect?.(url) - setOpen(false) + handleOpenChange(false) if (!pubkey || !/^https?:\/\//i.test(url)) return // Fire-and-forget: waiting on every relay can freeze the UI when relays are down. void publish(buildKind1063GifPublishDraft(url, desc), { @@ -181,7 +224,7 @@ export default function GifPicker({ }).catch(() => {}) if (desc) setPublishDescription('') }, - [pubkey, onSelect, publish, gif1063PublishRelayUrls, publishDescription] + [pubkey, onSelect, publish, gif1063PublishRelayUrls, publishDescription, handleOpenChange] ) const handleUpload = async (e: React.ChangeEvent) => { @@ -241,7 +284,7 @@ export default function GifPicker({ window.removeEventListener('message', handler) gifbuddyPopupRef.current = null onSelect?.(urlToInsert) - setOpen(false) + handleOpenChange(false) } } window.addEventListener('message', handler) @@ -250,7 +293,7 @@ export default function GifPicker({ gifbuddyPopupRef.current = null }, 10 * 60 * 1000) if (w) w.addEventListener('beforeunload', () => { clearTimeout(t); window.removeEventListener('message', handler) }) - }, [searchInput, onSelect]) + }, [searchInput, onSelect, handleOpenChange]) const descriptionForPublish = publishDescription.trim() @@ -260,7 +303,7 @@ export default function GifPicker({ if (!url || !/^https?:\/\//i.test(url)) return onSelect?.(url) setPasteUrl('') - setOpen(false) + handleOpenChange(false) if (pubkey) { setPublishingPaste(true) try { @@ -274,7 +317,7 @@ export default function GifPicker({ setPublishingPaste(false) } } - }, [pasteUrl, pubkey, onSelect, publish, gif1063PublishRelayUrls, descriptionForPublish]) + }, [pasteUrl, pubkey, onSelect, publish, gif1063PublishRelayUrls, descriptionForPublish, handleOpenChange]) /** External GIF from a note: publish kind 1063, then insert URL and close (same relays as grid pick). */ const handleArchiveAndInsert = useCallback( @@ -287,7 +330,7 @@ export default function GifPicker({ const desc = publishDescription.trim() setArchivingEventId(gif.eventId) onSelect?.(url) - setOpen(false) + handleOpenChange(false) void loadGifs(true) void publish(buildKind1063GifPublishDraft(url, desc), { specifiedRelayUrls: gif1063PublishRelayUrls @@ -298,7 +341,7 @@ export default function GifPicker({ if (desc) setPublishDescription('') }) }, - [pubkey, publish, gif1063PublishRelayUrls, onSelect, loadGifs, publishDescription] + [pubkey, publish, gif1063PublishRelayUrls, onSelect, loadGifs, publishDescription, handleOpenChange] ) const gifSourceKindTitle = useCallback( @@ -344,11 +387,14 @@ export default function GifPicker({ ))} ) : ( -
+
{gifs.map((gif) => { const showArchive = gifShouldOfferNip94Archive(gif) && isLoggedIn return ( -
+