From aa38e7c6006cdb46b74662c598e7c8911ff11409 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 15 Mar 2026 22:15:55 +0100 Subject: [PATCH] fix gif picker modal --- src/components/EmojiPickerDialog/index.tsx | 9 ++++-- src/components/GifPicker/index.tsx | 23 ++++++++++--- src/components/ui/dialog.tsx | 4 +-- src/components/ui/drawer.tsx | 13 +++++--- src/components/ui/dropdown-menu.tsx | 9 +++--- .../DiscussionsPage/CreateThreadDialog.tsx | 10 +++++- src/services/gif.service.ts | 32 +++++++++++++------ 7 files changed, 70 insertions(+), 30 deletions(-) diff --git a/src/components/EmojiPickerDialog/index.tsx b/src/components/EmojiPickerDialog/index.tsx index 4b597380..943668bc 100644 --- a/src/components/EmojiPickerDialog/index.tsx +++ b/src/components/EmojiPickerDialog/index.tsx @@ -11,10 +11,13 @@ import EmojiPicker from '../EmojiPicker' export default function EmojiPickerDialog({ children, - onEmojiClick + onEmojiClick, + portalContainer }: { children: React.ReactNode onEmojiClick?: (emoji: string | TEmoji | undefined) => void + /** When set (e.g. inside a modal), picker content portals here so it stays on top of the modal */ + portalContainer?: HTMLElement | null }) { const { isSmallScreen } = useScreenSize() const [open, setOpen] = useState(false) @@ -23,7 +26,7 @@ export default function EmojiPickerDialog({ return ( {children} - + Emoji Picker @@ -42,7 +45,7 @@ export default function EmojiPickerDialog({ return ( {children} - + { e.stopPropagation() diff --git a/src/components/GifPicker/index.tsx b/src/components/GifPicker/index.tsx index 62a16199..69ac41ff 100644 --- a/src/components/GifPicker/index.tsx +++ b/src/components/GifPicker/index.tsx @@ -12,7 +12,7 @@ import { useNostr } from '@/providers/NostrProvider' import { ExtendedKind, GIF_RELAY_URLS } from '@/constants' import { fetchGifs, searchGifs, type GifMetadata } from '@/services/gif.service' import mediaUpload from '@/services/media-upload.service' -import { Loader2 } from 'lucide-react' +import { Loader2, X } from 'lucide-react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,10 +20,13 @@ const GIFBUDDY_URL = 'https://www.gifbuddy.lol/' export default function GifPicker({ children, - onSelect + onSelect, + portalContainer }: { children: React.ReactNode onSelect?: (gifUrl: string) => void + /** When set (e.g. inside a modal), picker content portals here so it stays on top of the modal */ + portalContainer?: HTMLElement | null }) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() @@ -125,13 +128,23 @@ export default function GifPicker({ const content = (
-
+
setSearchInput(e.target.value)} className="flex-1" /> +
{error && (

{error}

@@ -207,7 +220,7 @@ export default function GifPicker({ return ( {children} - + {t('Choose a GIF')} @@ -220,7 +233,7 @@ export default function GifPicker({ return ( {children} - + {content} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 086a8b2f..a6ab20b7 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -58,7 +58,7 @@ const DialogOverlay = React.forwardRef< (({ className, ...props }, ref) => ( )) @@ -69,14 +69,17 @@ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName const DrawerContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & { hideOverlay?: boolean } ->(({ className, children, hideOverlay = false, ...props }, ref) => ( - + React.ComponentPropsWithoutRef & { + hideOverlay?: boolean + portalContainer?: HTMLElement | null + } +>(({ className, children, hideOverlay = false, portalContainer, ...props }, ref) => ( + {!hideOverlay && } { if (showScrollButtons) { @@ -138,8 +138,9 @@ const DropdownMenuContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { showScrollButtons?: boolean + portalContainer?: HTMLElement | null } ->(({ className, sideOffset = 4, showScrollButtons = false, ...props }, ref) => { +>(({ className, sideOffset = 4, showScrollButtons = false, portalContainer, ...props }, ref) => { const [canScrollUp, setCanScrollUp] = React.useState(false) const [canScrollDown, setCanScrollDown] = React.useState(false) const contentRef = React.useRef(null) @@ -178,12 +179,12 @@ const DropdownMenuContent = React.forwardRef< } return ( - + { if (showScrollButtons) { diff --git a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx index 4011a8ea..38c3c275 100644 --- a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx +++ b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx @@ -126,6 +126,7 @@ export default function CreateThreadDialog({ const [showAdvancedOptions, setShowAdvancedOptions] = useState(false) const [isLoadingRelays, setIsLoadingRelays] = useState(true) const [isTopicSelectorOpen, setIsTopicSelectorOpen] = useState(false) + const [pickerPortalContainer, setPickerPortalContainer] = useState(null) // Readings options state const [isReadingGroup, setIsReadingGroup] = useState(false) @@ -541,6 +542,12 @@ export default function CreateThreadDialog({ return (
+ {/* Portal target for GIF/emoji pickers so they render as children of this modal */} +
{t('Create New Thread')} @@ -710,13 +717,14 @@ export default function CreateThreadDialog({ {t('Upload Image')} - insertAtCursor(gifUrl)}> + insertAtCursor(gifUrl)} portalContainer={pickerPortalContainer}> { if (emoji == null) return const char = typeof emoji === 'string' ? emoji : (emoji as { native?: string }).native ?? String(emoji) diff --git a/src/services/gif.service.ts b/src/services/gif.service.ts index 1714b155..87f07616 100644 --- a/src/services/gif.service.ts +++ b/src/services/gif.service.ts @@ -27,21 +27,29 @@ function parseGifFromEvent(event: NEvent): GifMetadata | null { let fallbackUrl: string | undefined let sha256: string | undefined - // imeta tags (NIP-92) + // imeta tags (NIP-92): accept url when it contains .gif or when m is image/gif const imetaTags = event.tags.filter((t) => t[0] === 'imeta') for (const imetaTag of imetaTags) { + const mimeField = imetaTag.find((f) => f?.startsWith('m ')) + const imetaMime = mimeField?.substring(2).trim() + const isGifMime = imetaMime === 'image/gif' for (let i = 1; i < imetaTag.length; i++) { const field = imetaTag[i] if (field?.startsWith('url ')) { const candidateUrl = field.substring(4).trim() - if (candidateUrl && candidateUrl.toLowerCase().includes('.gif')) { + if (!candidateUrl) continue + const urlHasGif = candidateUrl.toLowerCase().includes('.gif') + if (urlHasGif || isGifMime) { url = candidateUrl - const mimeField = imetaTag.find((f) => f?.startsWith('m ')) - if (mimeField) mimeType = mimeField.substring(2).trim() - const xField = imetaTag.find((f) => f?.startsWith('x ')) - const yField = imetaTag.find((f) => f?.startsWith('y ')) - if (xField) width = parseInt(xField.substring(2).trim(), 10) - if (yField) height = parseInt(yField.substring(2).trim(), 10) + if (mimeField) mimeType = imetaMime + const dimField = imetaTag.find((f) => f?.startsWith('dim ')) + if (dimField) { + const dims = dimField.substring(4).trim().split('x') + if (dims.length >= 2) { + width = parseInt(dims[0], 10) + height = parseInt(dims[1], 10) + } + } break } } @@ -80,11 +88,15 @@ function parseGifFromEvent(event: NEvent): GifMetadata | null { } } - // url tag + // url tag (accept any URL; isGif check below uses mime from 'm' tag if URL has no .gif) if (!url) { const urlTag = event.tags.find((t) => t[0] === 'url' && t[1]) - if (urlTag?.[1] && urlTag[1].toLowerCase().includes('.gif')) { + if (urlTag?.[1]) { url = urlTag[1] + if (!mimeType) { + const mTag = event.tags.find((t) => t[0] === 'm' && t[1]) + mimeType = mTag?.[1] + } } }