diff --git a/components/carousel.js b/components/carousel.js
index 666c895b..1b5ed6dd 100644
--- a/components/carousel.js
+++ b/components/carousel.js
@@ -120,7 +120,11 @@ export function CarouselProvider ({ children }) {
media.current.set(src, { src, originalSrc, rel })
}, [media.current])
- const value = useMemo(() => ({ showCarousel, addMedia }), [showCarousel, addMedia])
+ const removeMedia = useCallback((src) => {
+ media.current.delete(src)
+ }, [media.current])
+
+ const value = useMemo(() => ({ showCarousel, addMedia, removeMedia }), [showCarousel, addMedia, removeMedia])
return {children}
}
diff --git a/components/embed.js b/components/embed.js
new file mode 100644
index 00000000..fa24a660
--- /dev/null
+++ b/components/embed.js
@@ -0,0 +1,204 @@
+import { memo, useEffect, useRef, useState } from 'react'
+import classNames from 'classnames'
+import useDarkMode from './dark-mode'
+import styles from './text.module.css'
+import { Button } from 'react-bootstrap'
+import { TwitterTweetEmbed } from 'react-twitter-embed'
+import YouTube from 'react-youtube'
+
+function TweetSkeleton ({ className }) {
+ return (
+
+ )
+}
+
+export const NostrEmbed = memo(function NostrEmbed ({ src, className, topLevel, id }) {
+ const [show, setShow] = useState(false)
+ const iframeRef = useRef(null)
+
+ useEffect(() => {
+ if (!iframeRef.current) return
+
+ const setHeightFromIframe = (e) => {
+ if (e.origin !== 'https://njump.me' || !e?.data?.height || e.source !== iframeRef.current.contentWindow) return
+ iframeRef.current.height = `${e.data.height}px`
+ }
+
+ window?.addEventListener('message', setHeightFromIframe)
+
+ // https://github.com/vercel/next.js/issues/39451
+ iframeRef.current.src = `https://njump.me/${id}?embed=yes`
+
+ return () => {
+ window?.removeEventListener('message', setHeightFromIframe)
+ }
+ }, [iframeRef.current])
+
+ return (
+
+
+ {!show &&
+
}
+
+ )
+})
+
+const SpotifyEmbed = function SpotifyEmbed ({ src, className }) {
+ const iframeRef = useRef(null)
+
+ // https://open.spotify.com/track/1KFxcj3MZrpBGiGA8ZWriv?si=f024c3aa52294aa1
+ // Remove any additional path segments
+ const url = new URL(src)
+ url.pathname = url.pathname.replace(/\/intl-\w+\//, '/')
+
+ useEffect(() => {
+ if (!iframeRef.current) return
+
+ const id = url.pathname.split('/').pop()
+
+ // https://developer.spotify.com/documentation/embeds/tutorials/using-the-iframe-api
+ window.onSpotifyIframeApiReady = (IFrameAPI) => {
+ const options = {
+ uri: `spotify:episode:${id}`
+ }
+ const callback = (EmbedController) => {}
+ IFrameAPI.createController(iframeRef.current, options, callback)
+ }
+
+ return () => { window.onSpotifyIframeApiReady = null }
+ }, [iframeRef.current, url.pathname])
+
+ return (
+
+
+
+ )
+}
+
+const Embed = memo(function Embed ({ src, provider, id, meta, className, topLevel, onError }) {
+ const [darkMode] = useDarkMode()
+ const [overflowing, setOverflowing] = useState(true)
+ const [show, setShow] = useState(false)
+
+ // This Twitter embed could use similar logic to the video embeds below
+ if (provider === 'twitter') {
+ return (
+
+ }
+ onLoad={() => setOverflowing(true)}
+ />
+ {overflowing && !show &&
+ }
+
+ )
+ }
+
+ if (provider === 'nostr') {
+ return (
+
+ )
+ }
+
+ if (provider === 'wavlake') {
+ return (
+
+
+
+ )
+ }
+
+ if (provider === 'spotify') {
+ return (
+
+ )
+ }
+
+ if (provider === 'youtube') {
+ return (
+
+
+
+ )
+ }
+
+ if (provider === 'rumble') {
+ return (
+
+ )
+ }
+
+ if (provider === 'peertube') {
+ return (
+
+ )
+ }
+
+ return null
+})
+
+export default Embed
diff --git a/components/form.js b/components/form.js
index 350d1d48..26a956af 100644
--- a/components/form.js
+++ b/components/form.js
@@ -393,7 +393,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
{tab !== 'write' &&
- {meta.value}
+ {meta.value}
}
diff --git a/components/media-or-link.js b/components/media-or-link.js
index e5ccc918..fa36842f 100644
--- a/components/media-or-link.js
+++ b/components/media-or-link.js
@@ -1,13 +1,9 @@
import styles from './text.module.css'
-import { useState, useEffect, useMemo, useCallback, memo, useRef } from 'react'
-import { decodeProxyUrl, IMGPROXY_URL_REGEXP, MEDIA_DOMAIN_REGEXP, parseEmbedUrl } from '@/lib/url'
+import { useState, useEffect, useMemo, useCallback, memo } from 'react'
+import { decodeProxyUrl, IMGPROXY_URL_REGEXP, MEDIA_DOMAIN_REGEXP } from '@/lib/url'
import { useMe } from './me'
-import { Button } from 'react-bootstrap'
import { UNKNOWN_LINK_REL } from '@/lib/constants'
import classNames from 'classnames'
-import { TwitterTweetEmbed } from 'react-twitter-embed'
-import YouTube from 'react-youtube'
-import useDarkMode from './dark-mode'
import { useCarousel } from './carousel'
function LinkRaw ({ href, children, src, rel }) {
@@ -23,9 +19,15 @@ function LinkRaw ({ href, children, src, rel }) {
)
}
-const Media = memo(function Media ({ src, bestResSrc, srcSet, sizes, width, height, onClick, onError, style, className, video }) {
+const Media = memo(function Media ({
+ src, bestResSrc, srcSet, sizes, width,
+ height, onClick, onError, style, className, video
+}) {
return (
-
+
{video
?