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 Toc from './table-of-contents'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { RootProvider } from './root'
|
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 { numWithUnits } from '@/lib/format'
|
||||||
import { useQuoteReply } from './use-quote-reply'
|
import { useQuoteReply } from './use-quote-reply'
|
||||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
|
@ -70,6 +70,7 @@ function ItemEmbed ({ item }) {
|
||||||
const [overflowing, setOverflowing] = useState(false)
|
const [overflowing, setOverflowing] = useState(false)
|
||||||
const [show, setShow] = 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+)/)
|
const twitter = item.url?.match(/^https?:\/\/(?:twitter|x)\.com\/(?:#!\/)?\w+\/status(?:es)?\/(?<id>\d+)/)
|
||||||
if (twitter?.groups?.id) {
|
if (twitter?.groups?.id) {
|
||||||
return (
|
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)
|
const { provider, id, meta } = parseEmbedUrl(item.url)
|
||||||
if (youtube?.groups?.id) {
|
|
||||||
|
if (provider === 'youtube') {
|
||||||
return (
|
return (
|
||||||
<div className={styles.youtubeContainerContainer}>
|
<div className={styles.videoWrapper}>
|
||||||
<YouTube
|
<YouTube
|
||||||
videoId={youtube.groups.id} className={styles.youtubeContainer} opts={{
|
videoId={id} className={styles.videoContainer} opts={{
|
||||||
playerVars: {
|
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)) {
|
if (item.url?.match(IMGPROXY_URL_REGEXP)) {
|
||||||
return <ZoomableImage src={item.url} rel={item.rel ?? UNKNOWN_LINK_REL} />
|
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 { toString } from 'mdast-util-to-string'
|
||||||
import copy from 'clipboard-copy'
|
import copy from 'clipboard-copy'
|
||||||
import ZoomableImage, { decodeOriginalUrl } from './image'
|
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 reactStringReplace from 'react-string-replace'
|
||||||
import { rehypeInlineCodeProperty } from '@/lib/md'
|
import { rehypeInlineCodeProperty } from '@/lib/md'
|
||||||
import { Button } from 'react-bootstrap'
|
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
|
// ignore errors like invalid URLs
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the link is to a youtube video, render the video
|
const videoWrapperStyles = {
|
||||||
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)
|
maxWidth: topLevel ? '640px' : '320px',
|
||||||
if (youtube?.groups?.id) {
|
margin: '0.5rem 0',
|
||||||
|
paddingRight: '15px'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { provider, id, meta } = parseEmbedUrl(href)
|
||||||
|
|
||||||
|
// Youtube video embed
|
||||||
|
if (provider === 'youtube') {
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: topLevel ? '640px' : '320px', paddingRight: '15px', margin: '0.5rem 0' }}>
|
<div style={videoWrapperStyles}>
|
||||||
<YouTube
|
<YouTube
|
||||||
videoId={youtube.groups.id} className={styles.youtubeContainer} opts={{
|
videoId={id} className={styles.videoContainer} opts={{
|
||||||
playerVars: {
|
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
|
// 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>
|
return <Img src={href} rel={rel ?? UNKNOWN_LINK_REL} {...props}>{children}</Img>
|
||||||
},
|
},
|
||||||
|
|
|
@ -237,7 +237,7 @@ img.fullScreen {
|
||||||
font-size: .85rem;
|
font-size: .85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.youtubeContainer {
|
.videoContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
@ -245,7 +245,7 @@ img.fullScreen {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.youtubeContainer iframe {
|
.videoContainer iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
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) {
|
export function stripTrailingSlash (uri) {
|
||||||
return uri.endsWith('/') ? uri.slice(0, -1) : 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
|
// unsafe-inline for styles is not ideal but okay if script-src is using nonces
|
||||||
"style-src 'self' a.stacker.news 'unsafe-inline'",
|
"style-src 'self' a.stacker.news 'unsafe-inline'",
|
||||||
"manifest-src 'self'",
|
"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,
|
"connect-src 'self' https: wss:" + devSrc,
|
||||||
// disable dangerous plugins like Flash
|
// disable dangerous plugins like Flash
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
|
|
|
@ -15,7 +15,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.youtubeContainer {
|
.videoWrapper {
|
||||||
|
max-width: 640px;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
@ -23,7 +28,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.youtubeContainer iframe {
|
.videoContainer iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -36,16 +41,11 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.youtubeContainerContainer {
|
|
||||||
max-width: 640px;
|
|
||||||
padding-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.twitterContainer:not(:first-child) {
|
.twitterContainer:not(:first-child) {
|
||||||
margin-top: .75rem;
|
margin-top: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.youtubeContainerContainer:not(:first-child) {
|
.videoWrapper:not(:first-child) {
|
||||||
margin-top: .75rem;
|
margin-top: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue