diff --git a/src/components/GifPicker/index.tsx b/src/components/GifPicker/index.tsx index bace3a89..699e8b70 100644 --- a/src/components/GifPicker/index.tsx +++ b/src/components/GifPicker/index.tsx @@ -11,6 +11,7 @@ import { ScrollArea } from '@/components/ui/scroll-area' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useNostr } from '@/providers/NostrProvider' import { ExtendedKind, GIF_RELAY_URLS } from '@/constants' +import { normalizeUrl } from '@/lib/url' import { fetchGifs, searchGifs, type GifMetadata } from '@/services/gif.service' import mediaUpload from '@/services/media-upload.service' import { ExternalLink, Loader2, X } from 'lucide-react' @@ -34,7 +35,7 @@ export default function GifPicker({ }) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() - const { publish, pubkey } = useNostr() + const { publish, pubkey, relayList } = useNostr() const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const [searchInput, setSearchInput] = useState('') @@ -45,17 +46,21 @@ export default function GifPicker({ const [uploadError, setUploadError] = useState(null) const [pasteUrl, setPasteUrl] = useState('') const [publishingPaste, setPublishingPaste] = useState(false) + const [publishDescription, setPublishDescription] = useState('') const searchTimeoutRef = useRef | null>(null) const fileInputRef = useRef(null) const gifbuddyPopupRef = useRef(null) + const userReadRelays = relayList?.read ?? [] + const userWriteRelays = relayList?.write ?? [] + const loadGifs = useCallback(async (q: string, forceRefresh = false) => { setError(null) setLoading(true) try { const results = q.trim() - ? await searchGifs(q.trim(), 50, forceRefresh) - : await fetchGifs(undefined, 50, forceRefresh) + ? await searchGifs(q.trim(), 50, forceRefresh, userReadRelays, pubkey ?? null) + : await fetchGifs(undefined, 50, forceRefresh, userReadRelays, pubkey ?? null) setGifs(results) if (results.length === 0 && !q.trim()) { setError( @@ -70,7 +75,7 @@ export default function GifPicker({ } finally { setLoading(false) } - }, [t]) + }, [t, userReadRelays, pubkey]) useEffect(() => { if (!open) return @@ -95,30 +100,37 @@ export default function GifPicker({ } const handleUpload = async (e: React.ChangeEvent) => { - const files = e.target.files - if (!files?.length || !pubkey) return + const file = e.target.files?.[0] + if (!file || !pubkey) return setUploadError(null) setUploading(true) try { - for (const file of Array.from(files)) { - if (!file.type.includes('gif') && !file.name.toLowerCase().endsWith('.gif')) { - setUploadError(t('{{name}} is not a GIF file', { name: file.name })) - continue - } - const { url } = await mediaUpload.upload(file) - const draft = { - kind: ExtendedKind.FILE_METADATA, - content: '', - tags: [ - ['file', url, file.type || 'image/gif', `size ${file.size}`], - ['url', url], - ['m', file.type || 'image/gif'], - ['t', 'gif'] - ], - created_at: Math.floor(Date.now() / 1000) - } - await publish(draft, { specifiedRelayUrls: GIF_RELAY_URLS }) + if (!file.type.includes('gif') && !file.name.toLowerCase().endsWith('.gif')) { + setUploadError(t('{{name}} is not a GIF file', { name: file.name })) + return } + const { url } = await mediaUpload.upload(file) + const draft = { + kind: ExtendedKind.FILE_METADATA, + content: publishDescription.trim(), + tags: [ + ['file', url, file.type || 'image/gif', `size ${file.size}`], + ['url', url], + ['m', file.type || 'image/gif'], + ['t', 'gif'] + ], + created_at: Math.floor(Date.now() / 1000) + } + const writeUrls = [...GIF_RELAY_URLS, ...userWriteRelays] + const seen = new Set() + const specifiedRelayUrls = writeUrls.filter((u) => { + const n = (normalizeUrl(u) ?? u).toLowerCase() + if (seen.has(n)) return false + seen.add(n) + return true + }) + await publish(draft, { specifiedRelayUrls }) + setPublishDescription('') setQuery('') await loadGifs('', true) } catch (err) { @@ -160,6 +172,8 @@ export default function GifPicker({ if (w) w.addEventListener('beforeunload', () => { clearTimeout(t); window.removeEventListener('message', handler) }) }, [searchInput, onSelect]) + const descriptionForPublish = publishDescription.trim() + /** Insert pasted GIF URL and publish kind 1063 so it's added to Nostr GIF library. */ const handlePasteUrlInsert = useCallback(async () => { const url = pasteUrl.trim() @@ -172,7 +186,7 @@ export default function GifPicker({ try { const draft = { kind: ExtendedKind.FILE_METADATA, - content: '', + content: descriptionForPublish, tags: [ ['url', url], ['m', 'image/gif'], @@ -180,14 +194,23 @@ export default function GifPicker({ ], created_at: Math.floor(Date.now() / 1000) } - await publish(draft, { specifiedRelayUrls: GIF_RELAY_URLS }) + const writeUrls = [...GIF_RELAY_URLS, ...userWriteRelays] + const seen = new Set() + const specifiedRelayUrls = writeUrls.filter((u) => { + const n = (normalizeUrl(u) ?? u).toLowerCase() + if (seen.has(n)) return false + seen.add(n) + return true + }) + await publish(draft, { specifiedRelayUrls }) + setPublishDescription('') } catch { // ignore; URL was still inserted } finally { setPublishingPaste(false) } } - }, [pasteUrl, pubkey, onSelect, publish]) + }, [pasteUrl, pubkey, onSelect, publish, userWriteRelays, descriptionForPublish]) /** In drawer mode we constrain height and make only the GIF grid scroll so the drawer doesn't "sink" */ const isDrawer = isSmallScreen @@ -257,6 +280,19 @@ export default function GifPicker({
+ {isLoggedIn && ( +
+ + setPublishDescription(e.target.value)} + className="min-w-0" + /> +
+ )}