Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 27 additions & 1 deletion components/embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,33 @@ const Embed = memo(function Embed ({ src, provider, id, meta, className, topLeve
</div>
)
}

if (provider === 'audio') {
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'
}}
>
🎵 {meta.title}
Copy link

Choose a reason for hiding this comment

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

is "notes" the best graphic for representing audio? lots of audio might not be music, strictly speaking...

Copy link
Member

Choose a reason for hiding this comment

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

While arguable, audio files are historically represented by a music note, so it's okay to use it.

But @pory-gone you should use an SVG instead of an emoji: https://remixicon.com/icon/file-music-line

</div>
)}
<audio controls preload='metadata' style={{ width: '100%' }} src={src}>
<source src={src} type={`audio/${meta?.audioType || 'mpeg'}`} />
Your browser does not support the audio element.
<a href={src} target='_blank' rel='noreferrer'>
Download audio file
</a>
</audio>
</div>
)
}
if (provider === 'peertube') {
return (
<div className={classNames(styles.videoWrapper, className)}>
Expand Down
84 changes: 51 additions & 33 deletions components/media-or-link.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ function LinkRaw ({ href, children, src, rel }) {

const Media = memo(function Media ({
src, bestResSrc, srcSet, sizes, width,
height, onClick, onError, style, className, video
height, onClick, onError, style, className, video, audio
}) {
const [loaded, setLoaded] = useState(!video)
const [loaded, setLoaded] = useState(!video && !audio)
const ref = useRef(null)

const handleLoadedMedia = () => {
Expand All @@ -45,29 +45,40 @@ const Media = memo(function Media ({
className={classNames(className, styles.mediaContainer, { [styles.loaded]: loaded })}
style={style}
>
{video
? <video
{audio
? <audio
ref={ref}
src={src}
preload={bestResSrc !== src ? 'metadata' : undefined}
controls
poster={bestResSrc !== src ? bestResSrc : undefined}
preload='metadata'
width={width}
height={height}
onError={onError}
onLoadedMetadata={handleLoadedMedia}
/>
Comment on lines +48 to 58
Copy link
Member

Choose a reason for hiding this comment

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

This way we would have a styled Audio component (parsed from link) and an unstyled audio container (from upload).

You have some choices:

  • Don't use Embed for audios
    • you wouldn't need an extension, but you'd need to fix the way you recognize it's an audio in useMediaHelper
  • Upload audio files without the ![]() markdown so it can be parsed as embed
    • you would also need to append the extension based on the mime type, because your parser checks for extensions, and then remove the extension when the embed... but then the raw link would be broken... mhh...
  • Audio component shared between embeds and uploads
    • still two logics for audios

: <img
ref={ref}
src={src}
srcSet={srcSet}
sizes={sizes}
width={width}
height={height}
onClick={onClick}
onError={onError}
onLoad={handleLoadedMedia}
/>}
: video
? <video
ref={ref}
src={src}
preload={bestResSrc !== src ? 'metadata' : undefined}
controls
poster={bestResSrc !== src ? bestResSrc : undefined}
width={width}
height={height}
onError={onError}
onLoadedMetadata={handleLoadedMedia}
/>
: <img
ref={ref}
src={src}
srcSet={srcSet}
sizes={sizes}
width={width}
height={height}
onClick={onClick}
onError={onError}
onLoad={handleLoadedMedia}
/>}
</div>
)
})
Expand Down Expand Up @@ -101,7 +112,7 @@ export default function MediaOrLink ({ linkFallback = true, ...props }) {
if (!media.src) return null

if (!error) {
if (media.image || media.video) {
if (media.image || media.video || media.audio) {
return (
<Media
{...media} onClick={handleClick} onError={handleError}
Expand All @@ -124,28 +135,34 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
const { dimensions, video, format, ...srcSetObj } = srcSetIntital || {}
const [isImage, setIsImage] = useState(video === false && trusted)
const [isVideo, setIsVideo] = useState(video)
const [isAudio, setIsAudio] = useState(false)
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
if (!showMedia || isVideo || isImage || isAudio) 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
setIsImage(true)
}).catch((e) => {
console.warn('Cannot decode image:', src, e)
})
const audio = document.createElement('audio')
audio.onloadedmetadata = () => {
setIsAudio(true)
setIsImage(false)
setIsVideo(false)
}
Comment on lines 144 to +155
Copy link
Member

Choose a reason for hiding this comment

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

Audio files can be reproduced as videos, so it will never create an audio element:

Image

(this is <video>)

audio.onerror = () => {
const img = new window.Image()
img.src = src
img.decode().then(() => {
setIsImage(true)
}).catch((e) => {
console.warn('Cannot decode image:', src, e)
})
}
audio.src = src
}
video.src = src

Expand All @@ -154,7 +171,7 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
video.onerror = null
video.src = ''
}
}, [src, setIsImage, setIsVideo, showMedia, isImage])
}, [src, setIsImage, setIsVideo, setIsAudio, showMedia, isImage, isAudio])

const srcSet = useMemo(() => {
if (Object.keys(srcSetObj).length === 0) return undefined
Expand Down Expand Up @@ -203,7 +220,8 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
style,
width,
height,
image: (!me?.privates?.imgproxyOnly || trusted) && showMedia && isImage && !isVideo,
video: !me?.privates?.imgproxyOnly && showMedia && isVideo
image: (!me?.privates?.imgproxyOnly || trusted) && showMedia && isImage && !isVideo && !isAudio,
video: !me?.privates?.imgproxyOnly && showMedia && isVideo,
audio: showMedia && isAudio
}
}
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
9 changes: 8 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,15 @@ 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 AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'opus']
Copy link

Choose a reason for hiding this comment

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

for some folks, the above lines might not be obviously fine; however, line 44 is about file types, while 37-42 are for MIME types, and there isn't any one-to-one mapping between categories of types.

Copy link
Member

Choose a reason for hiding this comment

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

Unused and not necessary

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']
export const BOUNTY_MIN = 1000
Expand Down
18 changes: 17 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 Expand Up @@ -266,6 +279,9 @@ export function isMisleadingLink (text, href) {
return misleading
}

// Add after IMG_URL_REGEXP
Copy link

Choose a reason for hiding this comment

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

I believe the above comment is forensically interesting [i.e., "look, Copilot is talking to the operator"], and probably does not belong in the production codebase.

export const AUDIO_URL_REGEXP = /^(https?:\/\/.*\.(?:mp3|wav|ogg|flac|aac|m4a))$/i
Comment on lines +282 to +283
Copy link
Member

Choose a reason for hiding this comment

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

Unused and not necessary, probably you got inspiration from the other regexes at the bottom, but they don't serve a purpose afaik


// eslint-disable-next-line
export const URL_REGEXP = /^((https?|ftp):\/\/)?(www.)?(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i

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