Embed Rumble Video (#1191)
* Render Rumble video in preview and posts * Display Rumble video * Remove workspace * Add util function * Use searchParam for id * Update check for Rumble * Update youtube match strings * fix hostname conditions --------- Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
parent
9c5bec06fb
commit
52f57f8ac5
|
@ -22,7 +22,7 @@ import Share from './share'
|
|||
import Toc from './table-of-contents'
|
||||
import Link from 'next/link'
|
||||
import { RootProvider } from './root'
|
||||
import { IMGPROXY_URL_REGEXP } from '@/lib/url'
|
||||
import { IMGPROXY_URL_REGEXP, parseEmbedUrl } from '@/lib/url'
|
||||
import { numWithUnits } from '@/lib/format'
|
||||
import { useQuoteReply } from './use-quote-reply'
|
||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||
|
@ -70,6 +70,7 @@ function ItemEmbed ({ item }) {
|
|||
const [overflowing, setOverflowing] = useState(false)
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
// This Twitter embed could use similar logic to the video embeds below
|
||||
const twitter = item.url?.match(/^https?:\/\/(?:twitter|x)\.com\/(?:#!\/)?\w+\/status(?:es)?\/(?<id>\d+)/)
|
||||
if (twitter?.groups?.id) {
|
||||
return (
|
||||
|
@ -83,14 +84,15 @@ function ItemEmbed ({ item }) {
|
|||
)
|
||||
}
|
||||
|
||||
const youtube = item.url?.match(/(https?:\/\/)?((www\.)?(youtube(-nocookie)?|youtube.googleapis)\.com.*(v\/|v=|vi=|vi\/|e\/|embed\/|user\/.*\/u\/\d+\/)|youtu\.be\/)(?<id>[_0-9a-z-]+)((?:\?|&)(?:t|start)=(?<start>\d+))?/i)
|
||||
if (youtube?.groups?.id) {
|
||||
const { provider, id, meta } = parseEmbedUrl(item.url)
|
||||
|
||||
if (provider === 'youtube') {
|
||||
return (
|
||||
<div className={styles.youtubeContainerContainer}>
|
||||
<div className={styles.videoWrapper}>
|
||||
<YouTube
|
||||
videoId={youtube.groups.id} className={styles.youtubeContainer} opts={{
|
||||
videoId={id} className={styles.videoContainer} opts={{
|
||||
playerVars: {
|
||||
start: youtube?.groups?.start
|
||||
start: meta?.start || 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -98,6 +100,20 @@ function ItemEmbed ({ item }) {
|
|||
)
|
||||
}
|
||||
|
||||
if (provider === 'rumble') {
|
||||
return (
|
||||
<div className={styles.videoWrapper}>
|
||||
<div className={styles.videoContainer}>
|
||||
<iframe
|
||||
title='Rumble Video'
|
||||
allowFullScreen=''
|
||||
src={meta?.href}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.url?.match(IMGPROXY_URL_REGEXP)) {
|
||||
return <ZoomableImage src={item.url} rel={item.rel ?? UNKNOWN_LINK_REL} />
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import Thumb from '@/svgs/thumb-up-fill.svg'
|
|||
import { toString } from 'mdast-util-to-string'
|
||||
import copy from 'clipboard-copy'
|
||||
import ZoomableImage, { decodeOriginalUrl } from './image'
|
||||
import { IMGPROXY_URL_REGEXP, parseInternalLinks } from '@/lib/url'
|
||||
import { IMGPROXY_URL_REGEXP, parseInternalLinks, parseEmbedUrl } from '@/lib/url'
|
||||
import reactStringReplace from 'react-string-replace'
|
||||
import { rehypeInlineCodeProperty } from '@/lib/md'
|
||||
import { Button } from 'react-bootstrap'
|
||||
|
@ -238,15 +238,22 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o
|
|||
// ignore errors like invalid URLs
|
||||
}
|
||||
|
||||
// if the link is to a youtube video, render the video
|
||||
const youtube = href.match(/(https?:\/\/)?((www\.)?(youtube(-nocookie)?|youtube.googleapis)\.com.*(v\/|v=|vi=|vi\/|e\/|embed\/|user\/.*\/u\/\d+\/)|youtu\.be\/)(?<id>[_0-9a-z-]+)((?:\?|&)(?:t|start)=(?<start>\d+))?/i)
|
||||
if (youtube?.groups?.id) {
|
||||
const videoWrapperStyles = {
|
||||
maxWidth: topLevel ? '640px' : '320px',
|
||||
margin: '0.5rem 0',
|
||||
paddingRight: '15px'
|
||||
}
|
||||
|
||||
const { provider, id, meta } = parseEmbedUrl(href)
|
||||
|
||||
// Youtube video embed
|
||||
if (provider === 'youtube') {
|
||||
return (
|
||||
<div style={{ maxWidth: topLevel ? '640px' : '320px', paddingRight: '15px', margin: '0.5rem 0' }}>
|
||||
<div style={videoWrapperStyles}>
|
||||
<YouTube
|
||||
videoId={youtube.groups.id} className={styles.youtubeContainer} opts={{
|
||||
videoId={id} className={styles.videoContainer} opts={{
|
||||
playerVars: {
|
||||
start: youtube?.groups?.start
|
||||
start: meta?.start || 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -254,6 +261,21 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o
|
|||
)
|
||||
}
|
||||
|
||||
// Rumble video embed
|
||||
if (provider === 'rumble') {
|
||||
return (
|
||||
<div style={videoWrapperStyles}>
|
||||
<div className={styles.videoContainer}>
|
||||
<iframe
|
||||
title='Rumble Video'
|
||||
allowFullScreen=''
|
||||
src={meta?.href}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// assume the link is an image which will fallback to link if it's not
|
||||
return <Img src={href} rel={rel ?? UNKNOWN_LINK_REL} {...props}>{children}</Img>
|
||||
},
|
||||
|
|
|
@ -237,7 +237,7 @@ img.fullScreen {
|
|||
font-size: .85rem;
|
||||
}
|
||||
|
||||
.youtubeContainer {
|
||||
.videoContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
|
@ -245,7 +245,7 @@ img.fullScreen {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.youtubeContainer iframe {
|
||||
.videoContainer iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
|
39
lib/url.js
39
lib/url.js
|
@ -52,6 +52,45 @@ export function parseInternalLinks (href) {
|
|||
}
|
||||
}
|
||||
|
||||
export function parseEmbedUrl (href) {
|
||||
const { hostname, pathname, searchParams } = new URL(href)
|
||||
|
||||
if (hostname.endsWith('youtube.com') && pathname.includes('/watch')) {
|
||||
return {
|
||||
provider: 'youtube',
|
||||
id: searchParams.get('v'),
|
||||
meta: {
|
||||
href,
|
||||
start: searchParams.get('t')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hostname.endsWith('youtu.be') && pathname.length > 1) {
|
||||
return {
|
||||
provider: 'youtube',
|
||||
id: pathname.slice(1), // remove leading slash
|
||||
meta: {
|
||||
href,
|
||||
start: searchParams.get('t')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hostname.endsWith('rumble.com') && pathname.includes('/embed')) {
|
||||
return {
|
||||
provider: 'rumble',
|
||||
id: null, // not required
|
||||
meta: {
|
||||
href
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Important to return empty object as default
|
||||
return {}
|
||||
}
|
||||
|
||||
export function stripTrailingSlash (uri) {
|
||||
return uri.endsWith('/') ? uri.slice(0, -1) : uri
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export function middleware (request) {
|
|||
// unsafe-inline for styles is not ideal but okay if script-src is using nonces
|
||||
"style-src 'self' a.stacker.news 'unsafe-inline'",
|
||||
"manifest-src 'self'",
|
||||
'frame-src www.youtube.com platform.twitter.com',
|
||||
'frame-src www.youtube.com platform.twitter.com rumble.com',
|
||||
"connect-src 'self' https: wss:" + devSrc,
|
||||
// disable dangerous plugins like Flash
|
||||
"object-src 'none'",
|
||||
|
|
|
@ -15,7 +15,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.youtubeContainer {
|
||||
.videoWrapper {
|
||||
max-width: 640px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.videoContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
|
@ -23,7 +28,7 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.youtubeContainer iframe {
|
||||
.videoContainer iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
@ -36,16 +41,11 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.youtubeContainerContainer {
|
||||
max-width: 640px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.twitterContainer:not(:first-child) {
|
||||
margin-top: .75rem;
|
||||
}
|
||||
|
||||
.youtubeContainerContainer:not(:first-child) {
|
||||
.videoWrapper:not(:first-child) {
|
||||
margin-top: .75rem;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue