151 feat youtube embed iframe (#176)
* youtube embed iframe temp commit * Fixes styling and code cleanup * lint * Now clicking between the pause and settings button doesn't trigger the parent * use modest branding (less yt logos) * Stop playing the video once there's a navigation event * Make sure the iframe is unmounted on any navigation event * fixes tests * lint
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import {getYoutubeVideoId} from './../../src/lib/strings'
|
||||||
import {
|
import {
|
||||||
extractEntities,
|
extractEntities,
|
||||||
detectLinkables,
|
detectLinkables,
|
||||||
@@ -487,3 +488,29 @@ describe('toShareUrl', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getYoutubeVideoId', () => {
|
||||||
|
it(' should return undefined for invalid youtube links', () => {
|
||||||
|
expect(getYoutubeVideoId('')).toBeUndefined()
|
||||||
|
expect(getYoutubeVideoId('https://www.google.com')).toBeUndefined()
|
||||||
|
expect(getYoutubeVideoId('https://www.youtube.com')).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
getYoutubeVideoId('https://www.youtube.com/channelName'),
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
getYoutubeVideoId('https://www.youtube.com/channel/channelName'),
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getYoutubeVideoId should return video id for valid youtube links', () => {
|
||||||
|
expect(getYoutubeVideoId('https://www.youtube.com/watch?v=videoId')).toBe(
|
||||||
|
'videoId',
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
getYoutubeVideoId(
|
||||||
|
'https://www.youtube.com/watch?v=videoId&feature=share',
|
||||||
|
),
|
||||||
|
).toBe('videoId')
|
||||||
|
expect(getYoutubeVideoId('https://youtu.be/videoId')).toBe('videoId')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
import {RootStoreModel} from './../../../src/state/models/root-store'
|
||||||
import {NavigationModel} from './../../../src/state/models/navigation'
|
import {NavigationModel} from './../../../src/state/models/navigation'
|
||||||
import * as flags from '../../../src/build-flags'
|
import * as flags from '../../../src/build-flags'
|
||||||
|
import AtpAgent from '@atproto/api'
|
||||||
|
import {DEFAULT_SERVICE} from '../../../src/state'
|
||||||
|
|
||||||
describe('NavigationModel', () => {
|
describe('NavigationModel', () => {
|
||||||
let model: NavigationModel
|
let model: NavigationModel
|
||||||
|
let rootStore: RootStoreModel
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
model = new NavigationModel()
|
rootStore = new RootStoreModel(new AtpAgent({service: DEFAULT_SERVICE}))
|
||||||
|
model = new NavigationModel(rootStore)
|
||||||
model.setTitle('0-0', 'title')
|
model.setTitle('0-0', 'title')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ import {
|
|||||||
ImagesLightbox,
|
ImagesLightbox,
|
||||||
ShellUiModel,
|
ShellUiModel,
|
||||||
} from './../../../src/state/models/shell-ui'
|
} from './../../../src/state/models/shell-ui'
|
||||||
|
import {RootStoreModel} from '../../../src/state'
|
||||||
|
import AtpAgent from '@atproto/api'
|
||||||
|
import {DEFAULT_SERVICE} from '../../../src/state'
|
||||||
|
|
||||||
describe('ShellUiModel', () => {
|
describe('ShellUiModel', () => {
|
||||||
let model: ShellUiModel
|
let model: ShellUiModel
|
||||||
|
let rootStore: RootStoreModel
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
model = new ShellUiModel()
|
rootStore = new RootStoreModel(new AtpAgent({service: DEFAULT_SERVICE}))
|
||||||
|
model = new ShellUiModel(rootStore)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@@ -270,6 +270,8 @@ PODS:
|
|||||||
- React-Core
|
- React-Core
|
||||||
- react-native-version-number (0.3.6):
|
- react-native-version-number (0.3.6):
|
||||||
- React
|
- React
|
||||||
|
- react-native-webview (11.26.1):
|
||||||
|
- React-Core
|
||||||
- React-perflogger (0.71.1)
|
- React-perflogger (0.71.1)
|
||||||
- React-RCTActionSheet (0.71.1):
|
- React-RCTActionSheet (0.71.1):
|
||||||
- React-Core/RCTActionSheetHeaders (= 0.71.1)
|
- React-Core/RCTActionSheetHeaders (= 0.71.1)
|
||||||
@@ -468,6 +470,7 @@ DEPENDENCIES:
|
|||||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||||
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
|
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
|
||||||
- react-native-version-number (from `../node_modules/react-native-version-number`)
|
- react-native-version-number (from `../node_modules/react-native-version-number`)
|
||||||
|
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||||
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||||
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
|
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
|
||||||
@@ -573,6 +576,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native-splash-screen"
|
:path: "../node_modules/react-native-splash-screen"
|
||||||
react-native-version-number:
|
react-native-version-number:
|
||||||
:path: "../node_modules/react-native-version-number"
|
:path: "../node_modules/react-native-version-number"
|
||||||
|
react-native-webview:
|
||||||
|
:path: "../node_modules/react-native-webview"
|
||||||
React-perflogger:
|
React-perflogger:
|
||||||
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
|
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
|
||||||
React-RCTActionSheet:
|
React-RCTActionSheet:
|
||||||
@@ -671,6 +676,7 @@ SPEC CHECKSUMS:
|
|||||||
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
|
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
|
||||||
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
||||||
react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f
|
react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f
|
||||||
|
react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
|
||||||
React-perflogger: ec8eef2a8f03ecfa6361c2c5fb9197ef4a29cc85
|
React-perflogger: ec8eef2a8f03ecfa6361c2c5fb9197ef4a29cc85
|
||||||
React-RCTActionSheet: a0c023b86cf4c862fa9c4eb0f6f91fbe878fb2de
|
React-RCTActionSheet: a0c023b86cf4c862fa9c4eb0f6f91fbe878fb2de
|
||||||
React-RCTAnimation: 168d53718c74153947c0109f55900faa64d79439
|
React-RCTAnimation: 168d53718c74153947c0109f55900faa64d79439
|
||||||
|
|||||||
@@ -73,6 +73,8 @@
|
|||||||
"react-native-uuid": "^2.0.1",
|
"react-native-uuid": "^2.0.1",
|
||||||
"react-native-version-number": "^0.3.6",
|
"react-native-version-number": "^0.3.6",
|
||||||
"react-native-web": "^0.17.7",
|
"react-native-web": "^0.17.7",
|
||||||
|
"react-native-webview": "^11.26.1",
|
||||||
|
"react-native-youtube-iframe": "^2.2.2",
|
||||||
"rn-fetch-blob": "^0.12.0",
|
"rn-fetch-blob": "^0.12.0",
|
||||||
"tlds": "^1.234.0",
|
"tlds": "^1.234.0",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
|
|||||||
@@ -268,3 +268,32 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
|
|||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {RootStoreModel} from './root-store'
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {TABS_ENABLED} from '../../build-flags'
|
import {TABS_ENABLED} from '../../build-flags'
|
||||||
import {segmentClient} from '../../lib/segmentClient'
|
import {segmentClient} from '../../lib/segmentClient'
|
||||||
@@ -228,8 +229,9 @@ export class NavigationModel {
|
|||||||
]
|
]
|
||||||
tabIndex = 0
|
tabIndex = 0
|
||||||
|
|
||||||
constructor() {
|
constructor(public rootStore: RootStoreModel) {
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {
|
||||||
|
rootStore: false,
|
||||||
serialize: false,
|
serialize: false,
|
||||||
hydrate: false,
|
hydrate: false,
|
||||||
})
|
})
|
||||||
@@ -263,6 +265,7 @@ export class NavigationModel {
|
|||||||
// =
|
// =
|
||||||
|
|
||||||
navigate(url: string, title?: string) {
|
navigate(url: string, title?: string) {
|
||||||
|
this.rootStore.emitNavigation()
|
||||||
this.tab.navigate(url, title)
|
this.tab.navigate(url, title)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,6 +303,7 @@ export class NavigationModel {
|
|||||||
// fixed tab helper function
|
// fixed tab helper function
|
||||||
// -prf
|
// -prf
|
||||||
switchTo(purpose: TabPurpose, reset: boolean) {
|
switchTo(purpose: TabPurpose, reset: boolean) {
|
||||||
|
this.rootStore.emitNavigation()
|
||||||
switch (purpose) {
|
switch (purpose) {
|
||||||
case TabPurpose.Notifs:
|
case TabPurpose.Notifs:
|
||||||
this.tabIndex = 2
|
this.tabIndex = 2
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export class RootStoreModel {
|
|||||||
agent: AtpAgent
|
agent: AtpAgent
|
||||||
log = new LogModel()
|
log = new LogModel()
|
||||||
session = new SessionModel(this)
|
session = new SessionModel(this)
|
||||||
nav = new NavigationModel()
|
nav = new NavigationModel(this)
|
||||||
shell = new ShellUiModel()
|
shell = new ShellUiModel(this)
|
||||||
me = new MeModel(this)
|
me = new MeModel(this)
|
||||||
onboard = new OnboardModel()
|
onboard = new OnboardModel()
|
||||||
profiles = new ProfilesViewModel(this)
|
profiles = new ProfilesViewModel(this)
|
||||||
@@ -158,6 +158,14 @@ export class RootStoreModel {
|
|||||||
DeviceEventEmitter.emit('session-dropped')
|
DeviceEventEmitter.emit('session-dropped')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onNavigation(handler: () => void): EmitterSubscription {
|
||||||
|
return DeviceEventEmitter.addListener('navigation', handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitNavigation() {
|
||||||
|
DeviceEventEmitter.emit('navigation')
|
||||||
|
}
|
||||||
|
|
||||||
// background fetch
|
// background fetch
|
||||||
// =
|
// =
|
||||||
// - we use this to poll for unread notifications, which is not "ideal" behavior but
|
// - we use this to poll for unread notifications, which is not "ideal" behavior but
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {RootStoreModel} from './root-store'
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {ProfileViewModel} from './profile-view'
|
import {ProfileViewModel} from './profile-view'
|
||||||
import {isObj, hasProp} from '../lib/type-guards'
|
import {isObj, hasProp} from '../lib/type-guards'
|
||||||
@@ -113,8 +114,12 @@ export class ShellUiModel {
|
|||||||
isComposerActive = false
|
isComposerActive = false
|
||||||
composerOpts: ComposerOpts | undefined
|
composerOpts: ComposerOpts | undefined
|
||||||
|
|
||||||
constructor() {
|
constructor(public rootStore: RootStoreModel) {
|
||||||
makeAutoObservable(this, {serialize: false, hydrate: false})
|
makeAutoObservable(this, {
|
||||||
|
serialize: false,
|
||||||
|
rootStore: false,
|
||||||
|
hydrate: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(): unknown {
|
serialize(): unknown {
|
||||||
@@ -152,6 +157,7 @@ export class ShellUiModel {
|
|||||||
| ReportAccountModal
|
| ReportAccountModal
|
||||||
| DeleteAccountModal,
|
| DeleteAccountModal,
|
||||||
) {
|
) {
|
||||||
|
this.rootStore.emitNavigation()
|
||||||
this.isModalActive = true
|
this.isModalActive = true
|
||||||
this.activeModal = modal
|
this.activeModal = modal
|
||||||
}
|
}
|
||||||
@@ -162,6 +168,7 @@ export class ShellUiModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) {
|
openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) {
|
||||||
|
this.rootStore.emitNavigation()
|
||||||
this.isLightboxActive = true
|
this.isLightboxActive = true
|
||||||
this.activeLightbox = lightbox
|
this.activeLightbox = lightbox
|
||||||
}
|
}
|
||||||
@@ -172,6 +179,7 @@ export class ShellUiModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openComposer(opts: ComposerOpts) {
|
openComposer(opts: ComposerOpts) {
|
||||||
|
this.rootStore.emitNavigation()
|
||||||
this.isComposerActive = true
|
this.isComposerActive = true
|
||||||
this.composerOpts = opts
|
this.composerOpts = opts
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,17 +22,21 @@ export const Link = observer(function Link({
|
|||||||
noFeedback,
|
noFeedback,
|
||||||
}: {
|
}: {
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
href: string
|
href?: string
|
||||||
title?: string
|
title?: string
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
noFeedback?: boolean
|
noFeedback?: boolean
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
handleLink(store, href, false)
|
if (href) {
|
||||||
|
handleLink(store, href, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const onLongPress = () => {
|
const onLongPress = () => {
|
||||||
handleLink(store, href, true)
|
if (href) {
|
||||||
|
handleLink(store, href, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (noFeedback) {
|
if (noFeedback) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {Text} from '../text/Text'
|
||||||
|
import {Image} from '../images/Image'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {usePalette} from '../../../lib/hooks/usePalette'
|
||||||
|
import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external'
|
||||||
|
|
||||||
|
const ExternalLinkEmbed = ({
|
||||||
|
link,
|
||||||
|
onImagePress,
|
||||||
|
}: {
|
||||||
|
link: PresentedExternal
|
||||||
|
onImagePress: () => void
|
||||||
|
}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{link.thumb ? (
|
||||||
|
<Image
|
||||||
|
uri={link.thumb}
|
||||||
|
style={styles.extImage}
|
||||||
|
onPress={onImagePress}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
<View style={styles.extInner}>
|
||||||
|
<Text type="md-bold" numberOfLines={2} style={[pal.text]}>
|
||||||
|
{link.title || link.uri}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
type="sm"
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[pal.textLight, styles.extUri]}>
|
||||||
|
{link.uri}
|
||||||
|
</Text>
|
||||||
|
{link.description ? (
|
||||||
|
<Text
|
||||||
|
type="sm"
|
||||||
|
numberOfLines={2}
|
||||||
|
style={[pal.text, styles.extDescription]}>
|
||||||
|
{link.description}
|
||||||
|
</Text>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
extInner: {
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
extImage: {
|
||||||
|
borderTopLeftRadius: 6,
|
||||||
|
borderTopRightRadius: 6,
|
||||||
|
width: '100%',
|
||||||
|
maxHeight: 200,
|
||||||
|
},
|
||||||
|
extUri: {
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
extDescription: {
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ExternalLinkEmbed
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import React, {useEffect} from 'react'
|
||||||
|
import {useState} from 'react'
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
StyleSheet,
|
||||||
|
Pressable,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
EmitterSubscription,
|
||||||
|
} from 'react-native'
|
||||||
|
import YoutubePlayer from 'react-native-youtube-iframe'
|
||||||
|
import {usePalette} from '../../../lib/hooks/usePalette'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import ExternalLinkEmbed from './ExternalLinkEmbed'
|
||||||
|
import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external'
|
||||||
|
import {useStores} from '../../../../state'
|
||||||
|
|
||||||
|
const YoutubeEmbed = ({
|
||||||
|
link,
|
||||||
|
videoId,
|
||||||
|
}: {
|
||||||
|
videoId: string
|
||||||
|
link: PresentedExternal
|
||||||
|
}) => {
|
||||||
|
const store = useStores()
|
||||||
|
const [displayVideoPlayer, setDisplayVideoPlayer] = useState(false)
|
||||||
|
const [playerDimensions, setPlayerDimensions] = useState({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
})
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const handlePlayButtonPressed = () => {
|
||||||
|
setDisplayVideoPlayer(true)
|
||||||
|
}
|
||||||
|
const handleOnLayout = (event: {
|
||||||
|
nativeEvent: {layout: {width: any; height: any}}
|
||||||
|
}) => {
|
||||||
|
setPlayerDimensions({
|
||||||
|
width: event.nativeEvent.layout.width,
|
||||||
|
height: event.nativeEvent.layout.height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
let sub: EmitterSubscription
|
||||||
|
if (displayVideoPlayer) {
|
||||||
|
sub = store.onNavigation(() => {
|
||||||
|
setDisplayVideoPlayer(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return () => sub && sub.remove()
|
||||||
|
}, [displayVideoPlayer, store])
|
||||||
|
|
||||||
|
if (!displayVideoPlayer) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[styles.extOuter, pal.view, pal.border]}
|
||||||
|
onLayout={handleOnLayout}>
|
||||||
|
<ExternalLinkEmbed link={link} onImagePress={handlePlayButtonPressed} />
|
||||||
|
<Pressable onPress={handlePlayButtonPressed} style={styles.playButton}>
|
||||||
|
<FontAwesomeIcon icon="play" size={24} color="white" />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const height = (playerDimensions.width / 16) * 9
|
||||||
|
const noop = () => {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableWithoutFeedback onPress={noop}>
|
||||||
|
<View>
|
||||||
|
{/* Removing the outter View will make tap events propagate to parents */}
|
||||||
|
<YoutubePlayer
|
||||||
|
initialPlayerParams={{
|
||||||
|
modestbranding: true,
|
||||||
|
}}
|
||||||
|
webViewProps={{
|
||||||
|
startInLoadingState: true,
|
||||||
|
}}
|
||||||
|
height={height}
|
||||||
|
videoId={videoId}
|
||||||
|
webViewStyle={styles.webView}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
extOuter: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
playButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '20%',
|
||||||
|
alignSelf: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'black',
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 50,
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
webView: {
|
||||||
|
alignItems: 'center',
|
||||||
|
alignContent: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default YoutubeEmbed
|
||||||
@@ -7,14 +7,16 @@ import {
|
|||||||
Image as RNImage,
|
Image as RNImage,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api'
|
import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../Link'
|
||||||
import {Text} from './text/Text'
|
import {Image} from '../images/Image'
|
||||||
import {Image} from './images/Image'
|
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
|
||||||
import {ImageLayoutGrid} from './images/ImageLayoutGrid'
|
import {ImagesLightbox} from '../../../../state/models/shell-ui'
|
||||||
import {ImagesLightbox} from '../../../state/models/shell-ui'
|
import {useStores} from '../../../../state'
|
||||||
import {useStores} from '../../../state'
|
import {usePalette} from '../../../lib/hooks/usePalette'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {saveImageModal} from '../../../../lib/images'
|
||||||
import {saveImageModal} from '../../../lib/images'
|
import YoutubeEmbed from './YoutubeEmbed'
|
||||||
|
import ExternalLinkEmbed from './ExternalLinkEmbed'
|
||||||
|
import {getYoutubeVideoId} from '../../../../lib/strings'
|
||||||
|
|
||||||
type Embed =
|
type Embed =
|
||||||
| AppBskyEmbedImages.Presented
|
| AppBskyEmbedImages.Presented
|
||||||
@@ -103,33 +105,18 @@ export function PostEmbeds({
|
|||||||
}
|
}
|
||||||
if (AppBskyEmbedExternal.isPresented(embed)) {
|
if (AppBskyEmbedExternal.isPresented(embed)) {
|
||||||
const link = embed.external
|
const link = embed.external
|
||||||
|
const youtubeVideoId = getYoutubeVideoId(link.uri)
|
||||||
|
|
||||||
|
if (youtubeVideoId) {
|
||||||
|
return <YoutubeEmbed videoId={youtubeVideoId} link={link} />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
style={[styles.extOuter, pal.view, pal.border, style]}
|
style={[styles.extOuter, pal.view, pal.border, style]}
|
||||||
href={link.uri}
|
href={link.uri}
|
||||||
noFeedback>
|
noFeedback>
|
||||||
{link.thumb ? (
|
<ExternalLinkEmbed link={link} />
|
||||||
<Image uri={link.thumb} style={styles.extImage} />
|
|
||||||
) : undefined}
|
|
||||||
<View style={styles.extInner}>
|
|
||||||
<Text type="md-bold" numberOfLines={2} style={[pal.text]}>
|
|
||||||
{link.title || link.uri}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
type="sm"
|
|
||||||
numberOfLines={1}
|
|
||||||
style={[pal.textLight, styles.extUri]}>
|
|
||||||
{link.uri}
|
|
||||||
</Text>
|
|
||||||
{link.description ? (
|
|
||||||
<Text
|
|
||||||
type="sm"
|
|
||||||
numberOfLines={2}
|
|
||||||
style={[pal.text, styles.extDescription]}>
|
|
||||||
{link.description}
|
|
||||||
</Text>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -149,19 +136,4 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
extInner: {
|
|
||||||
padding: 10,
|
|
||||||
},
|
|
||||||
extImage: {
|
|
||||||
borderTopLeftRadius: 6,
|
|
||||||
borderTopRightRadius: 6,
|
|
||||||
width: '100%',
|
|
||||||
maxHeight: 200,
|
|
||||||
},
|
|
||||||
extUri: {
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
extDescription: {
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
@@ -65,6 +65,8 @@ import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
|
|||||||
import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
|
import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
|
||||||
import {faX} from '@fortawesome/free-solid-svg-icons/faX'
|
import {faX} from '@fortawesome/free-solid-svg-icons/faX'
|
||||||
import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
|
import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
|
||||||
|
import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay'
|
||||||
|
import {faPause} from '@fortawesome/free-solid-svg-icons/faPause'
|
||||||
|
|
||||||
export function setup() {
|
export function setup() {
|
||||||
library.add(
|
library.add(
|
||||||
@@ -133,5 +135,7 @@ export function setup() {
|
|||||||
faTrashCan,
|
faTrashCan,
|
||||||
faX,
|
faX,
|
||||||
faXmark,
|
faXmark,
|
||||||
|
faPlay,
|
||||||
|
faPause,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5658,16 +5658,16 @@ escape-html@~1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||||
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
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:
|
escape-string-regexp@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||||
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
|
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:
|
escape-string-regexp@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||||
@@ -7254,7 +7254,7 @@ internal-slot@^1.0.3, internal-slot@^1.0.4:
|
|||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.4"
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
invariant@*, invariant@^2.2.4:
|
invariant@*, invariant@2.2.4, invariant@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||||
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
||||||
@@ -11503,6 +11503,21 @@ react-native-web@^0.17.7:
|
|||||||
normalize-css-color "^1.0.2"
|
normalize-css-color "^1.0.2"
|
||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
|
|
||||||
|
react-native-webview@^11.26.1:
|
||||||
|
version "11.26.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.26.1.tgz#658c09ed5162dc170b361e48c2dd26c9712879da"
|
||||||
|
integrity sha512-hC7BkxOpf+z0UKhxFSFTPAM4shQzYmZHoELa6/8a/MspcjEP7ukYKpuSUTLDywQditT8yI9idfcKvfZDKQExGw==
|
||||||
|
dependencies:
|
||||||
|
escape-string-regexp "2.0.0"
|
||||||
|
invariant "2.2.4"
|
||||||
|
|
||||||
|
react-native-youtube-iframe@^2.2.2:
|
||||||
|
version "2.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-youtube-iframe/-/react-native-youtube-iframe-2.2.2.tgz#ade1a2e4ead3d539fbb80463f45b59ff1b510b55"
|
||||||
|
integrity sha512-og2KW21kCwAHKcnWoyWWBYC6J2Xtqjjwpghhoy9G6zfwZkr8Ej27BbQIAKM/TheJJUZ5/YUrqsgqAdnFYDx5TQ==
|
||||||
|
dependencies:
|
||||||
|
events "^3.2.0"
|
||||||
|
|
||||||
react-native@0.71.1:
|
react-native@0.71.1:
|
||||||
version "0.71.1"
|
version "0.71.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.1.tgz#72b45af2b29e3d5a660c63425ab5003bf2112f99"
|
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.1.tgz#72b45af2b29e3d5a660c63425ab5003bf2112f99"
|
||||||
|
|||||||
Reference in New Issue
Block a user