Skip to content
Open
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
68 changes: 67 additions & 1 deletion components/embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,70 @@ import styles from './text.module.css'
import { Button } from 'react-bootstrap'
import { TwitterTweetEmbed } from 'react-twitter-embed'
import YouTube from 'react-youtube'
import FileMusicLine from '@/svgs/file-music-line.svg'

function AudioEmbed ({ src, meta, className }) {
const [error, setError] = useState(false)
const audioRef = useRef(null)

const handleError = (e) => {
console.warn('Audio loading error:', e)
setError(true)
}

if (error) {
return (
<div className={classNames(styles.audioWrapper, className)}>
<div style={{ padding: '1rem', textAlign: 'center' }}>
<p style={{ marginBottom: '0.5rem', color: 'var(--theme-color)' }}>
Unable to play this audio file.
</p>
<a
href={src}
target='_blank'
rel='noreferrer noopener'
style={{ color: 'var(--bs-primary)' }}
>
Download or open in new tab
</a>
</div>
</div>
)
}

return (
<div className={classNames(styles.audioWrapper, className)}>
{meta?.title && (
<div style={{
fontSize: '14px',
fontWeight: '500',
marginBottom: '8px',
color: 'var(--theme-color)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'flex',
alignItems: 'center',
gap: '6px'
}}
>
<FileMusicLine width={16} height={16} style={{ flexShrink: 0 }} />
{meta.title}
</div>
)}
<audio
ref={audioRef}
controls
preload='none'
style={{ width: '100%' }}
onError={handleError}
>
<source src={src} type={`audio/${meta?.audioType || 'mpeg'}`} />
Your browser does not support the audio element.
</audio>
</div>
)
}

function TweetSkeleton ({ className }) {
return (
Expand Down Expand Up @@ -195,7 +259,9 @@ const Embed = memo(function Embed ({ src, provider, id, meta, className, topLeve
</div>
)
}

if (provider === 'audio') {
return <AudioEmbed src={src} meta={meta} className={className} />
}
if (provider === 'peertube') {
return (
<div className={classNames(styles.videoWrapper, className)}>
Expand Down
6 changes: 1 addition & 5 deletions components/media-or-link.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,21 +127,17 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
const showMedia = useMemo(() => tab === 'preview' || me?.privates?.showImagesAndVideos !== false, [tab, me?.privates?.showImagesAndVideos])

useEffect(() => {
// don't load the video at all if user doesn't want these
if (!showMedia || isVideo || isImage) return

// check if it's a video by trying to load it
const video = document.createElement('video')
video.onloadedmetadata = () => {
setIsVideo(true)
setIsImage(false)
}
video.onerror = () => {
// hack
// if it's not a video it will throw an error, so we can assume it's an image
const img = new window.Image()
img.src = src
img.decode().then(() => { // decoding beforehand to prevent wrong image cropping
img.decode().then(() => {
setIsImage(true)
}).catch((e) => {
console.warn('Cannot decode image:', src, e)
Expand Down
63 changes: 61 additions & 2 deletions components/text.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@
margin-top: .25rem;
}

.text li > :is(.twitterContainer, .nostrContainer, .wavlakeWrapper, .spotifyWrapper, .onlyImages) {
.text li > :is(.twitterContainer, .nostrContainer, .wavlakeWrapper, .spotifyWrapper, .audioWrapper, .onlyImages) {
display: inline-flex;
vertical-align: top;
width: 100%;
Expand Down Expand Up @@ -319,12 +319,71 @@
font-size: smaller;
}

.twitterContainer, .nostrContainer, .videoWrapper, .wavlakeWrapper, .spotifyWrapper {
.twitterContainer, .nostrContainer, .videoWrapper, .wavlakeWrapper, .spotifyWrapper, .audioWrapper {
margin-top: calc(var(--grid-gap) * 0.5);
margin-bottom: calc(var(--grid-gap) * 0.5);
background-color: var(--theme-bg);
}

.audioWrapper {
width: 100%;
max-width: 500px;
padding: 0.75rem;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--theme-border);
background: var(--theme-bg);
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
margin: 0.5rem 0 !important;
transition: box-shadow 0.2s ease;
}

.audioWrapper:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.audioWrapper audio {
width: 100%;
height: 40px;
background: transparent;
border-radius: 8px;
outline: none;
}

.audioWrapper audio::-webkit-media-controls-panel {
background-color: var(--theme-bg);
border-radius: 6px;
}

.audioWrapper audio::-webkit-media-controls-play-button,
.audioWrapper audio::-webkit-media-controls-pause-button {
background-color: var(--bs-primary);
border-radius: 50%;
margin-right: 6px;
width: 32px;
height: 32px;
}

.audioWrapper audio::-webkit-media-controls-timeline {
background-color: var(--theme-border);
border-radius: 3px;
margin: 0 6px;
height: 4px;
}

.audioWrapper audio::-webkit-media-controls-current-time-display,
.audioWrapper audio::-webkit-media-controls-time-remaining-display {
color: var(--theme-color);
font-size: 11px;
font-family: monospace;
}

.topLevel .audioWrapper, :global(.topLevel) .audioWrapper {
max-width: 600px;
margin: 0.75rem 0 !important;
padding: 1rem;
}

.videoWrapper {
max-width: 320px;
}
Expand Down
8 changes: 7 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ export const UPLOAD_TYPES_ALLOW = [
'video/quicktime',
'video/mp4',
'video/mpeg',
'video/webm'
'video/webm',
'audio/mpeg',
'audio/wav',
'audio/ogg',
'audio/mp4',
'audio/aac',
'audio/flac'
]
export const AVATAR_TYPES_ALLOW = UPLOAD_TYPES_ALLOW.filter(t => t.startsWith('image/'))
export const INVOICE_ACTION_NOTIFICATION_TYPES = ['ITEM_CREATE', 'ZAP', 'DOWN_ZAP', 'POLL_VOTE', 'BOOST']
Expand Down
15 changes: 14 additions & 1 deletion lib/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,20 @@ export function parseEmbedUrl (href) {

const { hostname, pathname, searchParams } = new URL(href)

// nostr prefixes: [npub1, nevent1, nprofile1, note1]
const audioExtensions = /\.(mp3|wav|ogg|flac|aac|m4a|opus|webm)(\?.*)?$/i
if (pathname && audioExtensions.test(pathname)) {
const extension = pathname.match(audioExtensions)[1].toLowerCase()
return {
provider: 'audio',
id: null,
meta: {
href,
audioType: extension,
title: decodeURIComponent(pathname.split('/').pop().split('.')[0])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there no better guess for the reasonable text label of uncaptioned audio? I'm thinking, some combination of comment author, context [i.e. SN item ID], and filename only if it's not some hash or blob identifier, otherwise some reasonable timestamp.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR focuses on embedding audio from a link, this would just complicate things

}
}
}

const nostr = href.match(/\/(?<id>(?<type>npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)/)
if (nostr?.groups?.id) {
let id = nostr.groups.id
Expand Down
3 changes: 2 additions & 1 deletion pages/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ export default function Settings ({ ssrData }) {
<div className='d-flex align-items-center'>show images, video, and 3rd party embeds
<Info>
<ul>
<li>if checked and a link is an image, video or can be embedded in another way, we will do it</li>
<li>if checked and a link is an image, video, audio or can be embedded in another way, we will do it</li>
<li>we support embeds from following sites:</li>
<ul>
<li>njump.me</li>
Expand All @@ -503,6 +503,7 @@ export default function Settings ({ ssrData }) {
<li>wavlake.com</li>
<li>bitcointv.com</li>
<li>peertube.tv</li>
<li>direct audio files (.mp3, .wav, .ogg, .flac, .aac, .m4a, .opus)</li>
</ul>
</ul>
</Info>
Expand Down
1 change: 1 addition & 0 deletions svgs/file-music-line.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.