Compare commits
1 Commits
latest
...
embed-player
| Author | SHA1 | Date | |
|---|---|---|---|
| ff265204f3 |
@@ -146,6 +146,9 @@
|
||||
"react-native-version-number": "^0.3.6",
|
||||
"react-native-web": "~0.19.6",
|
||||
"react-native-web-linear-gradient": "^1.1.2",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "^13.6.2",
|
||||
"react-native-youtube-iframe": "^2.3.0",
|
||||
"react-responsive": "^9.0.2",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"sentry-expo": "~7.0.0",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
export type EmbedPlayerParams =
|
||||
| {type: 'youtube_video'; videoId: string; playerUri: string}
|
||||
| {type: 'twitch_live'; channelId: string; playerUri: string}
|
||||
| {type: 'spotify_album'; albumId: string; playerUri: string}
|
||||
| {
|
||||
type: 'spotify_playlist'
|
||||
playlistId: string
|
||||
playerUri: string
|
||||
}
|
||||
| {type: 'spotify_song'; songId: string; playerUri: string}
|
||||
|
||||
export function parseEmbedPlayerFromUrl(
|
||||
url: string,
|
||||
): EmbedPlayerParams | undefined {
|
||||
let urlp
|
||||
try {
|
||||
urlp = new URL(url)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// youtube
|
||||
if (urlp.hostname === 'youtu.be') {
|
||||
const videoId = urlp.pathname.split('/')[1]
|
||||
if (videoId) {
|
||||
return {
|
||||
type: 'youtube_video',
|
||||
videoId,
|
||||
playerUri: `https://www.youtube.com/embed/${videoId}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (urlp.hostname === 'www.youtube.com' || urlp.hostname === 'youtube.com') {
|
||||
const videoId = urlp.searchParams.get('v') as string
|
||||
if (videoId) {
|
||||
return {
|
||||
type: 'youtube_video',
|
||||
videoId,
|
||||
playerUri: `https://www.youtube.com/embed/${videoId}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// twitch
|
||||
if (urlp.hostname === 'twitch.tv' || urlp.hostname === 'www.twitch.tv') {
|
||||
const parts = urlp.pathname.split('/')
|
||||
if (parts.length === 2 && parts[1]) {
|
||||
return {
|
||||
type: 'twitch_live',
|
||||
channelId: parts[1],
|
||||
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=${parts[1]}&parent=localhost`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// spotify
|
||||
if (urlp.hostname === 'open.spotify.com') {
|
||||
const [_, type, id] = urlp.pathname.split('/')
|
||||
if (type && id) {
|
||||
if (type === 'playlist') {
|
||||
return {
|
||||
type: 'spotify_playlist',
|
||||
playlistId: id,
|
||||
playerUri: `https://open.spotify.com/embed/playlist/${id}`,
|
||||
}
|
||||
}
|
||||
if (type === 'album') {
|
||||
return {
|
||||
type: 'spotify_album',
|
||||
albumId: id,
|
||||
playerUri: `https://open.spotify.com/embed/album/${id}`,
|
||||
}
|
||||
}
|
||||
if (type === 'track') {
|
||||
return {
|
||||
type: 'spotify_song',
|
||||
songId: id,
|
||||
playerUri: `https://open.spotify.com/embed/track/${id}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,35 +139,6 @@ export function feedUriToHref(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function getYoutubeVideoId(link: string): string | undefined {
|
||||
let url
|
||||
try {
|
||||
url = new URL(link)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (
|
||||
url.hostname !== 'www.youtube.com' &&
|
||||
url.hostname !== 'youtube.com' &&
|
||||
url.hostname !== 'youtu.be'
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
if (url.hostname === 'youtu.be') {
|
||||
const videoId = url.pathname.split('/')[1]
|
||||
if (!videoId) {
|
||||
return undefined
|
||||
}
|
||||
return videoId
|
||||
}
|
||||
const videoId = url.searchParams.get('v') as string
|
||||
if (!videoId) {
|
||||
return undefined
|
||||
}
|
||||
return videoId
|
||||
}
|
||||
|
||||
export function linkRequiresWarning(uri: string, label: string) {
|
||||
const labelDomain = labelToDomain(label)
|
||||
if (!labelDomain) {
|
||||
|
||||
@@ -9,10 +9,8 @@ import {toNiceDomain} from 'lib/strings/url-helpers'
|
||||
|
||||
export const ExternalLinkEmbed = ({
|
||||
link,
|
||||
imageChild,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
imageChild?: React.ReactNode
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
@@ -45,7 +43,6 @@ export const ExternalLinkEmbed = ({
|
||||
source={{uri: link.thumb}}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
{imageChild}
|
||||
</View>
|
||||
) : undefined}
|
||||
<View
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Pressable,
|
||||
StyleProp,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {WebView} from 'react-native-webview'
|
||||
import YoutubePlayer from 'react-native-youtube-iframe'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {EmbedPlayerParams} from 'lib/strings/embed-player'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Text} from '../text/Text'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {isNative} from 'platform/detection'
|
||||
|
||||
const ALL_ORIGINS = ['*']
|
||||
|
||||
interface ShouldStartLoadRequest {
|
||||
url: string
|
||||
}
|
||||
|
||||
export function ExternalPlayerEmbed({
|
||||
link,
|
||||
params,
|
||||
style,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
params: EmbedPlayerParams
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const [isPlayerActive, setPlayerActive] = React.useState(false)
|
||||
const [dim, setDim] = React.useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
|
||||
// measure the layout to set sizing
|
||||
const onLayout = (event: {
|
||||
nativeEvent: {layout: {width: any; height: any}}
|
||||
}) => {
|
||||
setDim({
|
||||
width: event.nativeEvent.layout.width,
|
||||
height: event.nativeEvent.layout.height,
|
||||
})
|
||||
}
|
||||
|
||||
// calculate height for the player and the screen size
|
||||
const height = React.useMemo(() => {
|
||||
if (params.type === 'youtube_video') {
|
||||
return (dim.width / 16) * 9
|
||||
}
|
||||
if (params.type === 'spotify_song') {
|
||||
if (dim.width <= 300) {
|
||||
return 180
|
||||
}
|
||||
return 232
|
||||
}
|
||||
if (params.type === 'spotify_playlist') {
|
||||
return 420
|
||||
}
|
||||
if (params.type === 'spotify_album') {
|
||||
return 420
|
||||
}
|
||||
return dim.width
|
||||
}, [params.type, dim])
|
||||
|
||||
// HACK
|
||||
if (link.thumb && link.thumb.includes('bsky.public.url')) {
|
||||
link.thumb = link.thumb.replace(
|
||||
'https://bsky.public.url',
|
||||
'http://localhost:56022',
|
||||
)
|
||||
}
|
||||
|
||||
if (isPlayerActive) {
|
||||
return (
|
||||
<View style={[{marginTop: 4}, style]} onLayout={onLayout}>
|
||||
<EventStopper>
|
||||
<Player
|
||||
width={dim.width}
|
||||
height={height}
|
||||
link={link}
|
||||
params={params}
|
||||
onLeaveViewport={() => setPlayerActive(false)}
|
||||
/>
|
||||
</EventStopper>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
},
|
||||
pal.view,
|
||||
style,
|
||||
]}
|
||||
onPress={() => setPlayerActive(true)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={link.title}
|
||||
accessibilityHint=""
|
||||
onLayout={onLayout}>
|
||||
<Placeholder
|
||||
width={dim.width}
|
||||
height={height}
|
||||
link={link}
|
||||
params={params}
|
||||
isLoading={false}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
function Placeholder({
|
||||
width,
|
||||
height,
|
||||
params,
|
||||
link,
|
||||
isLoading,
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
params: EmbedPlayerParams
|
||||
isLoading: boolean
|
||||
}) {
|
||||
return (
|
||||
<View>
|
||||
{link.thumb ? (
|
||||
<Image
|
||||
style={{width, height, borderRadius: 6}}
|
||||
source={{uri: link.thumb}}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width,
|
||||
backgroundColor: '#000c',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 18,
|
||||
borderBottomLeftRadius: 6,
|
||||
borderBottomRightRadius: 6,
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
}}>
|
||||
<View style={{paddingTop: 6, width: 30}}>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<FontAwesomeIcon icon="play" size={24} color="white" />
|
||||
)}
|
||||
</View>
|
||||
<View style={{flex: 1}}>
|
||||
<Text type="lg-bold" numberOfLines={2} style={{color: '#fff'}}>
|
||||
{link.title || link.uri}
|
||||
</Text>
|
||||
{params.type.startsWith('youtube') && (
|
||||
<Text style={{color: '#fff'}}>YouTube</Text>
|
||||
)}
|
||||
{params.type.startsWith('twitch') && (
|
||||
<Text style={{color: '#fff'}}>Twitch.tv</Text>
|
||||
)}
|
||||
{params.type.startsWith('spotify') && (
|
||||
<Text style={{color: '#fff'}}>Spotify</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Player({
|
||||
width,
|
||||
height,
|
||||
link,
|
||||
params,
|
||||
onLeaveViewport,
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
params: EmbedPlayerParams
|
||||
onLeaveViewport: () => void
|
||||
}) {
|
||||
const ref = React.useRef<View>(null)
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
|
||||
// watch for leaving the viewport due to scrolling
|
||||
React.useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
ref.current?.measure((x, y, width, height, pageX, pageY) => {
|
||||
const window = Dimensions.get('window')
|
||||
const top = pageY
|
||||
const bot = pageY + height
|
||||
const isVisible = isNative
|
||||
? top >= 0 && bot <= window.height
|
||||
: !(top >= window.height || bot <= 0)
|
||||
if (!isVisible) {
|
||||
onLeaveViewport()
|
||||
}
|
||||
})
|
||||
}, 1e3)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [ref, onLeaveViewport])
|
||||
|
||||
// ensures we only load what's requested
|
||||
const onShouldStartLoadWithRequest = React.useCallback(
|
||||
(event: ShouldStartLoadRequest) => event.url === params.playerUri,
|
||||
[params.playerUri],
|
||||
)
|
||||
|
||||
// TODO: is this needed?
|
||||
const originWhitelist =
|
||||
params.type === 'spotify_album' ||
|
||||
params.type === 'spotify_playlist' ||
|
||||
params.type === 'spotify_song'
|
||||
? ALL_ORIGINS
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<View ref={ref} style={{height}}>
|
||||
{isNative && params.type === 'youtube_video' ? (
|
||||
<YoutubePlayer
|
||||
videoId={params.videoId}
|
||||
play
|
||||
height={height}
|
||||
onReady={() => setLoading(false)}
|
||||
/>
|
||||
) : (
|
||||
<WebView
|
||||
javaScriptEnabled={true}
|
||||
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
|
||||
originWhitelist={originWhitelist}
|
||||
mediaPlaybackRequiresUserAction={false}
|
||||
allowsInlineMediaPlayback
|
||||
bounces={false}
|
||||
allowsFullscreenVideo
|
||||
source={{uri: params.playerUri}}
|
||||
onLoad={() => setLoading(false)}
|
||||
/>
|
||||
)}
|
||||
{loading && (
|
||||
<View style={{position: 'absolute', left: 0, top: 0}}>
|
||||
<Placeholder
|
||||
width={width}
|
||||
height={height}
|
||||
params={params}
|
||||
link={link}
|
||||
isLoading
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {Link} from '../Link'
|
||||
|
||||
export const YoutubeEmbed = ({
|
||||
link,
|
||||
style,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
|
||||
const imageChild = (
|
||||
<View style={styles.playButton}>
|
||||
<FontAwesomeIcon icon="play" size={24} color="white" />
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<Link
|
||||
asAnchor
|
||||
style={[styles.extOuter, pal.view, pal.border, style]}
|
||||
href={link.uri}>
|
||||
<ExternalLinkEmbed link={link} imageChild={imageChild} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
extOuter: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
},
|
||||
playButton: {
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
alignItems: 'center',
|
||||
top: '44%',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'black',
|
||||
padding: 10,
|
||||
borderRadius: 50,
|
||||
opacity: 0.8,
|
||||
},
|
||||
webView: {
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
@@ -23,9 +23,9 @@ import {ImagesLightbox} from 'state/models/ui/shell'
|
||||
import {useStores} from 'state/index'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {YoutubeEmbed} from './YoutubeEmbed'
|
||||
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
||||
import {getYoutubeVideoId} from 'lib/strings/url-helpers'
|
||||
import {ExternalPlayerEmbed} from './ExternalPlayerEmbed'
|
||||
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
||||
import {MaybeQuoteEmbed} from './QuoteEmbed'
|
||||
import {AutoSizedImage} from '../images/AutoSizedImage'
|
||||
import {CustomFeedEmbed} from './CustomFeedEmbed'
|
||||
@@ -154,10 +154,17 @@ export function PostEmbeds({
|
||||
// =
|
||||
if (AppBskyEmbedExternal.isView(embed)) {
|
||||
const link = embed.external
|
||||
const youtubeVideoId = getYoutubeVideoId(link.uri)
|
||||
// TODO factor this out!
|
||||
const embedPlayerParams = parseEmbedPlayerFromUrl(link.uri)
|
||||
|
||||
if (youtubeVideoId) {
|
||||
return <YoutubeEmbed link={link} style={style} />
|
||||
if (embedPlayerParams) {
|
||||
return (
|
||||
<ExternalPlayerEmbed
|
||||
link={link}
|
||||
params={embedPlayerParams}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -8693,16 +8693,16 @@ escape-html@~1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
||||
|
||||
escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
|
||||
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
|
||||
|
||||
escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
|
||||
|
||||
escape-string-regexp@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
|
||||
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
|
||||
|
||||
escape-string-regexp@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||
@@ -10690,7 +10690,7 @@ interpret@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
|
||||
integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
|
||||
|
||||
invariant@*, invariant@^2.2.4:
|
||||
invariant@*, invariant@2.2.4, invariant@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
||||
@@ -15486,6 +15486,13 @@ qs@6.11.0:
|
||||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
qs@^6.5.1:
|
||||
version "6.11.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
|
||||
integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
|
||||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
query-string@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328"
|
||||
@@ -15833,6 +15840,13 @@ react-native-web-linear-gradient@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz#33f85f7085a0bb5ffa5106faf02ed105b92a9ed7"
|
||||
integrity sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A==
|
||||
|
||||
react-native-web-webview@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-web-webview/-/react-native-web-webview-1.0.2.tgz#c215efa70c17589f2c8d640b1f1dc669b18c6e02"
|
||||
integrity sha512-oNAYNuqUqeqTuAAdIejzDqvUtYA+k5lrvhUYmASdUznZNmyIaoQFA6OKoA4K9F3wdMvark42vUXkUWIp875ewg==
|
||||
dependencies:
|
||||
qs "^6.5.1"
|
||||
|
||||
react-native-web@~0.19.6:
|
||||
version "0.19.8"
|
||||
resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.19.8.tgz#46127f8b310148fde11e4fef67fe625603599d47"
|
||||
@@ -15847,6 +15861,21 @@ react-native-web@~0.19.6:
|
||||
postcss-value-parser "^4.2.0"
|
||||
styleq "^0.1.3"
|
||||
|
||||
react-native-webview@^13.6.2:
|
||||
version "13.6.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.6.2.tgz#0a9b18793e915add5b5dbdbf32509d7751b49167"
|
||||
integrity sha512-QzhQ5JCU+Nf2W285DtvCZOVQy/MkJXMwNDYPZvOWQbAOgxJMSSO+BtqXTMA1UPugDsko6PxJ0TxSlUwIwJijDg==
|
||||
dependencies:
|
||||
escape-string-regexp "2.0.0"
|
||||
invariant "2.2.4"
|
||||
|
||||
react-native-youtube-iframe@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-youtube-iframe/-/react-native-youtube-iframe-2.3.0.tgz#40ca8e55db929b91bfa8e8d30e411658cbc304c5"
|
||||
integrity sha512-M+z63xwXVtS4dX3k8PbtHUUcWN+gRZt6J1EtPE7Y60BMOB979KjpkdrHqeR96or9pNR2W8K5tQhIkMXW2jwo7Q==
|
||||
dependencies:
|
||||
events "^3.2.0"
|
||||
|
||||
react-native@0.72.5:
|
||||
version "0.72.5"
|
||||
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.72.5.tgz#2c343fa6f3ead362cf07376634a33a4078864357"
|
||||
|
||||
Reference in New Issue
Block a user