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:
Tom 2024-05-28 14:18:32 +01:00 committed by GitHub
parent 9c5bec06fb
commit 52f57f8ac5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 101 additions and 24 deletions

View File

@ -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} />
}

View File

@ -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>
},

View File

@ -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;

View File

@ -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
}

View File

@ -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'",

View File

@ -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;
}