Browse Source

bug-fixes

imwald
Silberengel 2 months ago
parent
commit
07a47e94ef
  1. 12
      src/components/Content/index.tsx
  2. 12
      src/components/ContentPreview/Content.tsx
  3. 110
      src/components/GifPicker/index.tsx
  4. 12
      src/components/ProfileAbout/index.tsx
  5. 5
      src/constants.ts
  6. 8
      src/lib/content-parser.ts

12
src/components/Content/index.tsx

@ -5,6 +5,7 @@ import { @@ -5,6 +5,7 @@ import {
EmbeddedHashtagParser,
EmbeddedLNInvoiceParser,
EmbeddedMentionParser,
EmbeddedPaytoParser,
EmbeddedUrlParser,
EmbeddedWebsocketUrlParser,
parseContent
@ -25,6 +26,7 @@ import { @@ -25,6 +26,7 @@ import {
EmbeddedNote,
EmbeddedWebsocketUrl
} from '../Embedded'
import PaytoLink from '../PaytoLink'
import Emoji from '../Emoji'
import ImageGallery from '../ImageGallery'
import MediaPlayer from '../MediaPlayer'
@ -90,6 +92,7 @@ export default function Content({ @@ -90,6 +92,7 @@ export default function Content({
const nodes = parseContent(_content, [
EmbeddedUrlParser,
EmbeddedLNInvoiceParser,
EmbeddedPaytoParser,
EmbeddedWebsocketUrlParser,
EmbeddedEventParser,
EmbeddedMentionParser,
@ -442,6 +445,15 @@ export default function Content({ @@ -442,6 +445,15 @@ export default function Content({
if (node.type === 'invoice') {
return <EmbeddedLNInvoice invoice={node.data} key={index} className="mt-2" />
}
if (node.type === 'payto') {
return (
<PaytoLink
key={index}
paytoUri={node.data}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
/>
)
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl url={node.data} key={index} />
}

12
src/components/ContentPreview/Content.tsx

@ -2,6 +2,7 @@ import { @@ -2,6 +2,7 @@ import {
EmbeddedEmojiParser,
EmbeddedEventParser,
EmbeddedMentionParser,
EmbeddedPaytoParser,
EmbeddedUrlParser,
parseContent
} from '@/lib/content-parser'
@ -10,6 +11,7 @@ import { cn } from '@/lib/utils' @@ -10,6 +11,7 @@ import { cn } from '@/lib/utils'
import { TEmoji } from '@/types'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import PaytoLink from '../PaytoLink'
import { EmbeddedMentionText } from '../Embedded'
import Emoji from '../Emoji'
@ -26,6 +28,7 @@ export default function Content({ @@ -26,6 +28,7 @@ export default function Content({
const nodes = useMemo(() => {
return parseContent(content, [
EmbeddedUrlParser,
EmbeddedPaytoParser,
EmbeddedEventParser,
EmbeddedMentionParser,
EmbeddedEmojiParser
@ -47,6 +50,15 @@ export default function Content({ @@ -47,6 +50,15 @@ export default function Content({
if (node.type === 'mention') {
return <EmbeddedMentionText key={index} userId={node.data.split(':')[1]} />
}
if (node.type === 'payto') {
return (
<PaytoLink
key={index}
paytoUri={node.data}
className="text-green-600 dark:text-green-400 hover:underline break-words"
/>
)
}
if (node.type === 'emoji') {
const shortcode = node.data.slice(1, -1).trim()
const emoji = emojiInfos?.find((e) => e.shortcode === shortcode)

110
src/components/GifPicker/index.tsx

@ -6,17 +6,21 @@ import { @@ -6,17 +6,21 @@ import {
} from '@/components/ui/dropdown-menu'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
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 { fetchGifs, searchGifs, type GifMetadata } from '@/services/gif.service'
import mediaUpload from '@/services/media-upload.service'
import { Loader2, X } from 'lucide-react'
import { ExternalLink, Loader2, X } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
const GIFBUDDY_URL = 'https://www.gifbuddy.lol/'
/** Query param gifbuddy may use for pre-filled search (common convention). */
const GIFBUDDY_SEARCH_URL = (q: string) =>
q.trim() ? `${GIFBUDDY_URL}gifsearch?q=${encodeURIComponent(q.trim())}` : GIFBUDDY_URL
export default function GifPicker({
children,
@ -39,8 +43,11 @@ export default function GifPicker({ @@ -39,8 +43,11 @@ export default function GifPicker({
const [error, setError] = useState<string | null>(null)
const [uploading, setUploading] = useState(false)
const [uploadError, setUploadError] = useState<string | null>(null)
const [pasteUrl, setPasteUrl] = useState('')
const [publishingPaste, setPublishingPaste] = useState(false)
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const fileInputRef = useRef<HTMLInputElement | null>(null)
const gifbuddyPopupRef = useRef<Window | null>(null)
const loadGifs = useCallback(async (q: string, forceRefresh = false) => {
setError(null)
@ -126,6 +133,62 @@ export default function GifPicker({ @@ -126,6 +133,62 @@ export default function GifPicker({
const isLoggedIn = !!pubkey
/** 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 w = window.open(url, '_blank', 'noopener,noreferrer')
gifbuddyPopupRef.current = w ?? null
const handler = (event: MessageEvent) => {
if (event.origin !== 'https://www.gifbuddy.lol' && event.origin !== 'https://gifbuddy.lol') return
const data = event.data
const urlToInsert =
typeof data === 'string' && (data.startsWith('http://') || data.startsWith('https://'))
? data
: data?.url ?? data?.gifUrl
if (urlToInsert && typeof urlToInsert === 'string') {
window.removeEventListener('message', handler)
gifbuddyPopupRef.current = null
onSelect?.(urlToInsert)
setOpen(false)
}
}
window.addEventListener('message', handler)
const t = setTimeout(() => {
window.removeEventListener('message', handler)
gifbuddyPopupRef.current = null
}, 10 * 60 * 1000)
if (w) w.addEventListener('beforeunload', () => { clearTimeout(t); window.removeEventListener('message', handler) })
}, [searchInput, onSelect])
/** Insert pasted GIF URL and publish kind 1063 so it's added to Nostr GIF library. */
const handlePasteUrlInsert = useCallback(async () => {
const url = pasteUrl.trim()
if (!url || !/^https?:\/\//i.test(url)) return
onSelect?.(url)
setPasteUrl('')
setOpen(false)
if (pubkey) {
setPublishingPaste(true)
try {
const draft = {
kind: ExtendedKind.FILE_METADATA,
content: '',
tags: [
['url', url],
['m', 'image/gif'],
['t', 'gif']
],
created_at: Math.floor(Date.now() / 1000)
}
await publish(draft, { specifiedRelayUrls: GIF_RELAY_URLS })
} catch {
// ignore; URL was still inserted
} finally {
setPublishingPaste(false)
}
}
}, [pasteUrl, pubkey, onSelect, publish])
/** In drawer mode we constrain height and make only the GIF grid scroll so the drawer doesn't "sink" */
const isDrawer = isSmallScreen
const content = (
@ -194,14 +257,43 @@ export default function GifPicker({ @@ -194,14 +257,43 @@ export default function GifPicker({
</ScrollArea>
</div>
<div className="flex flex-col gap-2 border-t pt-2 shrink-0">
<a
href={GIFBUDDY_URL}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:underline text-center"
>
{t('Search GifBuddy for more GIFs')}
</a>
<div className="flex flex-col gap-1.5">
<Button
type="button"
variant="outline"
size="sm"
className="w-full"
onClick={openGifBuddySearch}
>
<ExternalLink className="size-3.5 mr-1.5" />
{t('Search on GifBuddy')}
</Button>
<p className="text-xs text-muted-foreground">
{t('Opens in a new tab. Copy a GIF URL there, then paste below. If this picker closed, click “Insert GIF” again to paste.')}
</p>
<div className="grid gap-1">
<Label className="text-xs text-muted-foreground">
{t('Paste URL of a GIF')}
</Label>
<div className="flex gap-1">
<Input
placeholder="https://..."
value={pasteUrl}
onChange={(e) => setPasteUrl(e.target.value)}
className="flex-1 min-w-0"
/>
<Button
type="button"
size="sm"
disabled={!pasteUrl.trim() || publishingPaste}
onClick={handlePasteUrlInsert}
title={t('Insert URL into your post and publish to Nostr GIF library (NIP-94).')}
>
{publishingPaste ? t('Adding…') : t('Insert')}
</Button>
</div>
</div>
</div>
{isLoggedIn && (
<>
<input

12
src/components/ProfileAbout/index.tsx

@ -1,10 +1,12 @@ @@ -1,10 +1,12 @@
import {
EmbeddedHashtagParser,
EmbeddedMentionParser,
EmbeddedPaytoParser,
EmbeddedUrlParser,
EmbeddedWebsocketUrlParser,
parseContent
} from '@/lib/content-parser'
import PaytoLink from '@/components/PaytoLink'
import {
EmbeddedHashtag,
EmbeddedMention,
@ -16,6 +18,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla @@ -16,6 +18,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla
const aboutNodes = parseContent(about ?? '', [
EmbeddedWebsocketUrlParser,
EmbeddedUrlParser,
EmbeddedPaytoParser,
EmbeddedHashtagParser,
EmbeddedMentionParser
]).map((node, index) => {
@ -25,6 +28,15 @@ export default function ProfileAbout({ about, className }: { about?: string; cla @@ -25,6 +28,15 @@ export default function ProfileAbout({ about, className }: { about?: string; cla
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl key={index} url={node.data} />
}
if (node.type === 'payto') {
return (
<PaytoLink
key={index}
paytoUri={node.data}
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline break-words"
/>
)
}
if (node.type === 'hashtag') {
return <EmbeddedHashtag key={index} hashtag={node.data} />
}

5
src/constants.ts

@ -122,12 +122,13 @@ export const FAST_WRITE_RELAY_URLS = [ @@ -122,12 +122,13 @@ export const FAST_WRITE_RELAY_URLS = [
]
/** Relays used for NIP-94 file metadata (kind 1063) / GIF discovery and publish.
* Include relay.gifbuddy.lol (GifBuddy) so we get many kind 1063 GIFs; damus/primal/thecitadel have fewer. */
* Publish to all of these so GIFs are discoverable across clients; some may be temporarily down. */
export const GIF_RELAY_URLS = [
'wss://relay.gifbuddy.lol',
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://thecitadel.nostr1.com'
'wss://thecitadel.nostr1.com',
'wss://nos.lol',
]
export const SEARCHABLE_RELAY_URLS = [

8
src/lib/content-parser.ts

@ -8,6 +8,7 @@ import { @@ -8,6 +8,7 @@ import {
WS_URL_REGEX,
YOUTUBE_URL_REGEX
} from '@/constants'
import { PAYTO_URI_REGEX } from '@/lib/payto'
import { isImage, isMedia } from './url'
export type TEmbeddedNodeType =
@ -24,6 +25,7 @@ export type TEmbeddedNodeType = @@ -24,6 +25,7 @@ export type TEmbeddedNodeType =
| 'emoji'
| 'invoice'
| 'youtube'
| 'payto'
export type TEmbeddedNode =
| {
@ -74,6 +76,12 @@ export const EmbeddedLNInvoiceParser: TContentParser = { @@ -74,6 +76,12 @@ export const EmbeddedLNInvoiceParser: TContentParser = {
regex: LN_INVOICE_REGEX
}
/** payto:// URIs (RFC-8905 / NIP-A3) – e.g. in profile about or note content */
export const EmbeddedPaytoParser: TContentParser = {
type: 'payto',
regex: PAYTO_URI_REGEX
}
export const EmbeddedUrlParser: TContentParser = (content: string) => {
const matches = content.matchAll(URL_REGEX)
const result: TEmbeddedNode[] = []

Loading…
Cancel
Save