Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 169 additions & 93 deletions components/embed.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,48 @@
import { memo, useEffect, useRef, useState } from 'react'
import { memo, useEffect, useRef, useState, useCallback } 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'
import { useIsClient } from './use-client'
import LoadErrorIcon from '@/svgs/file-warning-line.svg'
import Moon from '@/svgs/moon-fill.svg'
import Link from 'next/link'

function TweetSkeleton ({ className }) {
const Loading = ({ provider, src, className, error }) => {
let host = provider
try {
host = new URL(src).hostname
} catch (e) {
console.error(e)
}
return (
<div className={classNames(styles.embedLoading, className)}>
<div className={styles.embedLoadingMessage}>
{error ? <LoadErrorIcon className='fill-grey' /> : <Moon className='spin fill-grey' />}
<div>{error ? `${provider} embed is not available at the moment.` : `loading ${provider}...`}</div>
<Link href={src} target='_blank' rel='noopener nofollow noreferrer'>
view on {host}
</Link>
</div>
</div>
)
}

const LoaderWrapper = ({ loading, provider, src, children }) => {
console.log('loading', loading)
return (
<>
{loading && <Loading provider={provider} src={src} />}
<div style={{ display: loading ? 'none' : 'block' }}>
{children}
</div>
</>
)
}

const TweetSkeleton = ({ className }) => {
return (
<div className={classNames(styles.tweetsSkeleton, className)}>
<div className={styles.tweetSkeleton}>
Expand All @@ -21,9 +57,51 @@ function TweetSkeleton ({ className }) {
)
}

export const NostrEmbed = memo(function NostrEmbed ({ src, className, topLevel, darkMode, id }) {
const TwitterEmbed = ({ id, className, topLevel, ...props }) => {
const [darkMode] = useDarkMode()
const [overflowing, setOverflowing] = useState(true)
const [show, setShow] = useState(false)

return (
<div className={classNames(styles.twitterContainer, !show && styles.twitterContained, className)}>
<TwitterTweetEmbed
tweetId={id}
options={{ theme: darkMode ? 'dark' : 'light', width: topLevel ? '550px' : '350px' }}
key={darkMode ? '1' : '2'}
placeholder={<TweetSkeleton className={className} />}
onLoad={() => { props.onLoad(); setOverflowing(true) }}
onError={props.onError}
/>
{overflowing && !show &&
<Button size='lg' variant='info' className={styles.twitterShowFull} onClick={() => setShow(true)}>
show full tweet
</Button>}
</div>
)
}

const YouTubeEmbed = ({ id, className, start, ...props }) => {
return (
<div className={classNames(styles.videoWrapper, className)}>
<YouTube
videoId={id}
className={styles.videoContainer}
opts={{
playerVars: {
start: start || 0
}
}}
onReady={props.onLoad}
onError={props.onError}
/>
</div>
)
}

const NostrEmbed = ({ className, topLevel, id, ...props }) => {
const iframeRef = useRef(null)
const [darkMode] = useDarkMode()
const [show, setShow] = useState(false)

useEffect(() => {
if (!iframeRef.current) return
Expand Down Expand Up @@ -52,7 +130,7 @@ export const NostrEmbed = memo(function NostrEmbed ({ src, className, topLevel,
window?.removeEventListener('message', setHeightFromIframe)
iframeRef.current?.removeEventListener('load', handleIframeLoad)
}
}, [iframeRef.current, darkMode])
}, [id, darkMode])

return (
<div className={classNames(styles.nostrContainer, !show && styles.twitterContained, className)}>
Expand All @@ -64,6 +142,7 @@ export const NostrEmbed = memo(function NostrEmbed ({ src, className, topLevel,
frameBorder='0'
sandbox='allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox'
allow=''
{...props}
/>
{!show &&
<Button size='md' variant='info' className={styles.twitterShowFull} onClick={() => setShow(true)}>
Expand All @@ -72,18 +151,19 @@ export const NostrEmbed = memo(function NostrEmbed ({ src, className, topLevel,
</Button>}
</div>
)
})
}

const SpotifyEmbed = function SpotifyEmbed ({ src, className }) {
const SpotifyEmbed = function SpotifyEmbed ({ src, className, ...props }) {
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
console.log('useEffect', iframeRef.current)
console.log('useEffect', url.pathname)

const id = url.pathname.split('/').pop()

Expand Down Expand Up @@ -112,106 +192,102 @@ const SpotifyEmbed = function SpotifyEmbed ({ src, className }) {
allow='encrypted-media; clipboard-write;'
style={{ borderRadius: '12px' }}
sandbox='allow-scripts allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-presentation'
{...props}
/>
</div>
)
}

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 (
<>
<div className={classNames(styles.twitterContainer, !show && styles.twitterContained, className)}>
<TwitterTweetEmbed
tweetId={id}
options={{ theme: darkMode ? 'dark' : 'light', width: topLevel ? '550px' : '350px' }}
key={darkMode ? '1' : '2'}
placeholder={<TweetSkeleton className={className} />}
onLoad={() => setOverflowing(true)}
/>
{overflowing && !show &&
<Button size='lg' variant='info' className={styles.twitterShowFull} onClick={() => setShow(true)}>
show full tweet
</Button>}
</div>
</>
)
}

if (provider === 'nostr') {
return (
<NostrEmbed src={src} className={className} topLevel={topLevel} id={id} darkMode={darkMode} />
)
}
const WavlakeEmbed = ({ id, className, ...props }) => {
return (
<div className={classNames(styles.wavlakeWrapper, className)}>
<iframe
src={`https://embed.wavlake.com/track/${id}`} width='100%' height='380' frameBorder='0'
allow='encrypted-media'
sandbox='allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-same-origin'
{...props}
/>
</div>
)
}

if (provider === 'wavlake') {
return (
<div className={classNames(styles.wavlakeWrapper, className)}>
const PeerTubeEmbed = ({ className, href, ...props }) => {
return (
<div className={classNames(styles.videoWrapper, className)}>
<div className={styles.videoContainer}>
<iframe
src={`https://embed.wavlake.com/track/${id}`} width='100%' height='380' frameBorder='0'
allow='encrypted-media'
sandbox='allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-same-origin'
title='PeerTube Video'
allowFullScreen
src={href}
sandbox='allow-scripts'
{...props}
/>
</div>
)
}

if (provider === 'spotify') {
return (
<SpotifyEmbed src={src} className={className} />
)
}
</div>
)
}

if (provider === 'youtube') {
return (
<div className={classNames(styles.videoWrapper, className)}>
<YouTube
videoId={id} className={styles.videoContainer} opts={{
playerVars: {
start: meta?.start || 0
}
}}
const RumbleEmbed = ({ className, href, ...props }) => {
return (
<div className={classNames(styles.videoWrapper, className)}>
<div className={styles.videoContainer}>
<iframe
title='Rumble Video'
allowFullScreen
src={href}
sandbox='allow-scripts'
{...props}
/>
</div>
)
}
</div>
)
}

if (provider === 'rumble') {
return (
<div className={classNames(styles.videoWrapper, className)}>
<div className={styles.videoContainer}>
<iframe
title='Rumble Video'
allowFullScreen
src={meta?.href}
sandbox='allow-scripts'
/>
</div>
</div>
)
}
export default memo(function Embed ({ src, provider, id, meta, className, topLevel }) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
const onLoad = useCallback(() => setLoading(false), [])
const onError = useCallback(() => setError(true), [])
const props = { onLoad, onError }
const isClient = useIsClient()

if (provider === 'peertube') {
return (
<div className={classNames(styles.videoWrapper, className)}>
<div className={styles.videoContainer}>
<iframe
title='PeerTube Video'
allowFullScreen
src={meta?.href}
sandbox='allow-scripts'
/>
</div>
</div>
)
// after 15 seconds of loading, bail out
useEffect(() => {
if (loading) {
const timeout = setTimeout(() => {
setError(true)
}, 15000)

return () => clearTimeout(timeout)
}
}, [loading])

if (!isClient || error) {
return <Loading provider={provider} src={src} className={className} error={error} />
}

return null
return (
<LoaderWrapper loading={loading} provider={provider} src={src}>
{(() => {
switch (provider) {
case 'twitter':
return <TwitterEmbed id={id} className={className} topLevel={topLevel} {...props} />
case 'youtube':
return <YouTubeEmbed id={id} className={className} start={meta?.start} {...props} />
case 'nostr':
return <NostrEmbed className={className} topLevel={topLevel} id={id} {...props} />
case 'spotify':
return <SpotifyEmbed src={src} className={className} {...props} />
case 'wavlake':
return <WavlakeEmbed id={id} className={className} {...props} />
case 'peertube':
return <PeerTubeEmbed className={className} href={meta?.href} {...props} />
case 'rumble':
return <RumbleEmbed className={className} href={meta?.href} {...props} />
default:
return null
}
})()}
</LoaderWrapper>
)
})

export default Embed
20 changes: 19 additions & 1 deletion components/text.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -385,14 +385,32 @@
padding: .75rem 1rem;
}

.tweetsSkeleton {
.tweetsSkeleton, .embedLoading {
display: flex;
flex-flow: row wrap;
max-width: 350px;
width: 100%;
padding-right: 12px;
}

.embedLoadingMessage {
justify-content: center;
display: flex;
flex-direction: column;
border-radius: 0.4rem;
height: 150px;
width: 100%;
padding: 1.5rem;
background-color: var(--theme-commentBg);
}

.embedLoadingMessage svg {
width: 24px;
height: 24px;
margin-bottom: 1rem;
margin-left: -0.15rem;
}

.topLevel .tweetsSkeleton, :global(.topLevel) .tweetsSkeleton {
max-width: 550px;
}
Expand Down