diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/App.native.tsx | 79 | ||||
-rw-r--r-- | src/App.web.tsx | 73 | ||||
-rw-r--r-- | src/components/icons/Play.tsx | 9 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 62 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 11 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/ActiveVideoContext.tsx | 48 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbed.tsx | 44 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbedInner.tsx | 138 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbedInner.web.tsx | 52 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/VideoPlayerContext.tsx | 41 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/VideoPlayerContext.web.tsx | 9 |
11 files changed, 461 insertions, 105 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index ed76c753b..d2c20fc8e 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -23,10 +23,12 @@ import { } from '#/lib/statsig/statsig' import {s} from '#/lib/styles' import {ThemeProvider} from '#/lib/ThemeContext' +import I18nProvider from '#/locale/i18nProvider' import {logger} from '#/logger' import {Provider as A11yProvider} from '#/state/a11y' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' +import {listenSessionDropped} from '#/state/events' import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as LightboxStateProvider} from '#/state/lightbox' import {MessagesProvider} from '#/state/messages' @@ -49,6 +51,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {TestCtrls} from '#/view/com/testing/TestCtrls' +import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext' import * as Toast from '#/view/com/util/Toast' import {Shell} from '#/view/shell' import {ThemeProvider as Alf} from '#/alf' @@ -58,8 +61,6 @@ import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' import {Provider as TourProvider} from '#/tours' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' -import I18nProvider from './locale/i18nProvider' -import {listenSessionDropped} from './state/events' SplashScreen.preventAutoHideAsync() @@ -107,42 +108,44 @@ function InnerApp() { <Alf theme={theme}> <ThemeProvider theme={theme}> <Splash isReady={isReady && hasCheckedReferrer}> - <RootSiblingParent> - <React.Fragment - // Resets the entire tree below when it changes: - key={currentAccount?.did}> - <QueryProvider currentDid={currentAccount?.did}> - <StatsigProvider> - <MessagesProvider> - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - <LabelDefsProvider> - <ModerationOptsProvider> - <LoggedOutViewProvider> - <SelectedFeedProvider> - <UnreadNotifsProvider> - <BackgroundNotificationPreferencesProvider> - <MutedThreadsProvider> - <TourProvider> - <ProgressGuideProvider> - <GestureHandlerRootView - style={s.h100pct}> - <TestCtrls /> - <Shell /> - </GestureHandlerRootView> - </ProgressGuideProvider> - </TourProvider> - </MutedThreadsProvider> - </BackgroundNotificationPreferencesProvider> - </UnreadNotifsProvider> - </SelectedFeedProvider> - </LoggedOutViewProvider> - </ModerationOptsProvider> - </LabelDefsProvider> - </MessagesProvider> - </StatsigProvider> - </QueryProvider> - </React.Fragment> - </RootSiblingParent> + <ActiveVideoProvider> + <RootSiblingParent> + <React.Fragment + // Resets the entire tree below when it changes: + key={currentAccount?.did}> + <QueryProvider currentDid={currentAccount?.did}> + <StatsigProvider> + <MessagesProvider> + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + <LabelDefsProvider> + <ModerationOptsProvider> + <LoggedOutViewProvider> + <SelectedFeedProvider> + <UnreadNotifsProvider> + <BackgroundNotificationPreferencesProvider> + <MutedThreadsProvider> + <TourProvider> + <ProgressGuideProvider> + <GestureHandlerRootView + style={s.h100pct}> + <TestCtrls /> + <Shell /> + </GestureHandlerRootView> + </ProgressGuideProvider> + </TourProvider> + </MutedThreadsProvider> + </BackgroundNotificationPreferencesProvider> + </UnreadNotifsProvider> + </SelectedFeedProvider> + </LoggedOutViewProvider> + </ModerationOptsProvider> + </LabelDefsProvider> + </MessagesProvider> + </StatsigProvider> + </QueryProvider> + </React.Fragment> + </RootSiblingParent> + </ActiveVideoProvider> </Splash> </ThemeProvider> </Alf> diff --git a/src/App.web.tsx b/src/App.web.tsx index a64988f38..df6fbf244 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -12,10 +12,12 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler' import {QueryProvider} from '#/lib/react-query' import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {ThemeProvider} from '#/lib/ThemeContext' +import I18nProvider from '#/locale/i18nProvider' import {logger} from '#/logger' import {Provider as A11yProvider} from '#/state/a11y' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' +import {listenSessionDropped} from '#/state/events' import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as LightboxStateProvider} from '#/state/lightbox' import {MessagesProvider} from '#/state/messages' @@ -37,6 +39,7 @@ import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' +import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext' import * as Toast from '#/view/com/util/Toast' import {ToastContainer} from '#/view/com/util/Toast.web' import {Shell} from '#/view/shell/index' @@ -46,8 +49,6 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as PortalProvider} from '#/components/Portal' import {Provider as TourProvider} from '#/tours' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' -import I18nProvider from './locale/i18nProvider' -import {listenSessionDropped} from './state/events' function InnerApp() { const [isReady, setIsReady] = React.useState(false) @@ -92,39 +93,41 @@ function InnerApp() { <Alf theme={theme}> <ThemeProvider theme={theme}> <RootSiblingParent> - <React.Fragment - // Resets the entire tree below when it changes: - key={currentAccount?.did}> - <QueryProvider currentDid={currentAccount?.did}> - <StatsigProvider> - <MessagesProvider> - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - <LabelDefsProvider> - <ModerationOptsProvider> - <LoggedOutViewProvider> - <SelectedFeedProvider> - <UnreadNotifsProvider> - <BackgroundNotificationPreferencesProvider> - <MutedThreadsProvider> - <SafeAreaProvider> - <TourProvider> - <ProgressGuideProvider> - <Shell /> - </ProgressGuideProvider> - </TourProvider> - </SafeAreaProvider> - </MutedThreadsProvider> - </BackgroundNotificationPreferencesProvider> - </UnreadNotifsProvider> - </SelectedFeedProvider> - </LoggedOutViewProvider> - </ModerationOptsProvider> - </LabelDefsProvider> - </MessagesProvider> - </StatsigProvider> - </QueryProvider> - </React.Fragment> - <ToastContainer /> + <ActiveVideoProvider> + <React.Fragment + // Resets the entire tree below when it changes: + key={currentAccount?.did}> + <QueryProvider currentDid={currentAccount?.did}> + <StatsigProvider> + <MessagesProvider> + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + <LabelDefsProvider> + <ModerationOptsProvider> + <LoggedOutViewProvider> + <SelectedFeedProvider> + <UnreadNotifsProvider> + <BackgroundNotificationPreferencesProvider> + <MutedThreadsProvider> + <SafeAreaProvider> + <TourProvider> + <ProgressGuideProvider> + <Shell /> + </ProgressGuideProvider> + </TourProvider> + </SafeAreaProvider> + </MutedThreadsProvider> + </BackgroundNotificationPreferencesProvider> + </UnreadNotifsProvider> + </SelectedFeedProvider> + </LoggedOutViewProvider> + </ModerationOptsProvider> + </LabelDefsProvider> + </MessagesProvider> + </StatsigProvider> + </QueryProvider> + </React.Fragment> + <ToastContainer /> + </ActiveVideoProvider> </RootSiblingParent> </ThemeProvider> </Alf> diff --git a/src/components/icons/Play.tsx b/src/components/icons/Play.tsx new file mode 100644 index 000000000..acf421d57 --- /dev/null +++ b/src/components/icons/Play.tsx @@ -0,0 +1,9 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Play_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z', +}) + +export const Play_Filled_Corner2_Rounded = createSinglePathSVG({ + path: 'M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z', +}) diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index a05339d4d..425a2257f 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -210,38 +210,40 @@ function PostInner({ </View> )} <LabelsOnMyPost post={post} /> - <ContentHider - modui={moderation.ui('contentView')} - style={styles.contentHider} - childContainerStyle={styles.contentHiderChild}> - <PostAlerts + {false && ( + <ContentHider modui={moderation.ui('contentView')} - style={[a.py_xs]} - /> - {richText.text ? ( - <View style={styles.postTextContainer}> - <RichText - enableTags - testID="postText" - value={richText} - numberOfLines={limitLines ? MAX_POST_LINES : undefined} - style={[a.flex_1, a.text_md]} - authorHandle={post.author.handle} - /> - </View> - ) : undefined} - {limitLines ? ( - <TextLink - text={_(msg`Show More`)} - style={pal.link} - onPress={onPressShowMore} - href="#" + style={styles.contentHider} + childContainerStyle={styles.contentHiderChild}> + <PostAlerts + modui={moderation.ui('contentView')} + style={[a.py_xs]} /> - ) : undefined} - {post.embed ? ( - <PostEmbeds embed={post.embed} moderation={moderation} /> - ) : null} - </ContentHider> + {richText.text ? ( + <View style={styles.postTextContainer}> + <RichText + enableTags + testID="postText" + value={richText} + numberOfLines={limitLines ? MAX_POST_LINES : undefined} + style={[a.flex_1, a.text_md]} + authorHandle={post.author.handle} + /> + </View> + ) : undefined} + {limitLines ? ( + <TextLink + text={_(msg`Show More`)} + style={pal.link} + onPress={onPressShowMore} + href="#" + /> + ) : undefined} + {post.embed ? ( + <PostEmbeds embed={post.embed} moderation={moderation} /> + ) : null} + </ContentHider> + )} <PostCtrls post={post} record={record} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index dbc5796db..b5e208011 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -16,8 +16,10 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' +import {useGate} from '#/lib/statsig/statsig' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {useFeedFeedbackContext} from '#/state/feed-feedback' +import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types' import {MAX_POST_LINES} from 'lib/constants' @@ -29,6 +31,7 @@ import {countLines} from 'lib/strings/helpers' import {s} from 'lib/styles' import {precacheProfile} from 'state/queries/profile' import {atoms as a} from '#/alf' +import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' import {ContentHider} from '#/components/moderation/ContentHider' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichText} from '#/components/RichText' @@ -38,13 +41,12 @@ import {FeedNameText} from '../util/FeedInfoText' import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link' import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostEmbeds} from '../util/post-embeds' +import {VideoEmbed} from '../util/post-embeds/VideoEmbed' import {PostMeta} from '../util/PostMeta' import {Text} from '../util/text/Text' import {PreviewableUserAvatar} from '../util/UserAvatar' import {AviFollowButton} from './AviFollowButton' import hairlineWidth = StyleSheet.hairlineWidth -import {useSession} from '#/state/session' -import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' interface FeedItemProps { record: AppBskyFeedPost.Record @@ -136,6 +138,8 @@ let FeedItemInner = ({ const {openComposer} = useComposerControls() const pal = usePalette('default') const {_} = useLingui() + const gate = useGate() + const href = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) @@ -354,6 +358,9 @@ let FeedItemInner = ({ postAuthor={post.author} onOpenEmbed={onOpenEmbed} /> + {__DEV__ && gate('videos') && ( + <VideoEmbed source="https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8" /> + )} <PostCtrls post={post} record={record} diff --git a/src/view/com/util/post-embeds/ActiveVideoContext.tsx b/src/view/com/util/post-embeds/ActiveVideoContext.tsx new file mode 100644 index 000000000..6804436a7 --- /dev/null +++ b/src/view/com/util/post-embeds/ActiveVideoContext.tsx @@ -0,0 +1,48 @@ +import React, {useCallback, useId, useMemo, useState} from 'react' + +import {VideoPlayerProvider} from './VideoPlayerContext' + +const ActiveVideoContext = React.createContext<{ + activeViewId: string | null + setActiveView: (viewId: string, src: string) => void +} | null>(null) + +export function ActiveVideoProvider({children}: {children: React.ReactNode}) { + const [activeViewId, setActiveViewId] = useState<string | null>(null) + const [source, setSource] = useState<string | null>(null) + + const value = useMemo( + () => ({ + activeViewId, + setActiveView: (viewId: string, src: string) => { + setActiveViewId(viewId) + setSource(src) + }, + }), + [activeViewId], + ) + + return ( + <ActiveVideoContext.Provider value={value}> + <VideoPlayerProvider source={source ?? ''} viewId={activeViewId}> + {children} + </VideoPlayerProvider> + </ActiveVideoContext.Provider> + ) +} + +export function useActiveVideoView() { + const context = React.useContext(ActiveVideoContext) + if (!context) { + throw new Error('useActiveVideo must be used within a ActiveVideoProvider') + } + const id = useId() + + return { + active: context.activeViewId === id, + setActive: useCallback( + (source: string) => context.setActiveView(id, source), + [context, id], + ), + } +} diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx new file mode 100644 index 000000000..5e5293a55 --- /dev/null +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -0,0 +1,44 @@ +import React, {useCallback} from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' +import {useActiveVideoView} from './ActiveVideoContext' +import {VideoEmbedInner} from './VideoEmbedInner' + +export function VideoEmbed({source}: {source: string}) { + const t = useTheme() + const {active, setActive} = useActiveVideoView() + const {_} = useLingui() + + const onPress = useCallback(() => setActive(source), [setActive, source]) + + return ( + <View + style={[ + a.w_full, + a.rounded_sm, + {aspectRatio: 16 / 9}, + a.overflow_hidden, + t.atoms.bg_contrast_25, + a.my_xs, + ]}> + {active ? ( + <VideoEmbedInner source={source} /> + ) : ( + <Button + style={[a.flex_1, t.atoms.bg_contrast_25]} + onPress={onPress} + label={_(msg`Play video`)} + variant="ghost" + color="secondary" + size="large"> + <ButtonIcon icon={PlayIcon} /> + </Button> + )} + </View> + ) +} diff --git a/src/view/com/util/post-embeds/VideoEmbedInner.tsx b/src/view/com/util/post-embeds/VideoEmbedInner.tsx new file mode 100644 index 000000000..ef0678709 --- /dev/null +++ b/src/view/com/util/post-embeds/VideoEmbedInner.tsx @@ -0,0 +1,138 @@ +import React, {useCallback, useEffect, useRef, useState} from 'react' +import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native' +import Animated, { + measure, + runOnJS, + useAnimatedRef, + useFrameCallback, + useSharedValue, +} from 'react-native-reanimated' +import {VideoPlayer, VideoView} from 'expo-video' + +import {atoms as a} from '#/alf' +import {Text} from '#/components/Typography' +import {useVideoPlayer} from './VideoPlayerContext' + +export const VideoEmbedInner = ({}: {source: string}) => { + const player = useVideoPlayer() + const aref = useAnimatedRef<Animated.View>() + const {height: windowHeight} = useWindowDimensions() + const hasLeftView = useSharedValue(false) + const ref = useRef<VideoView>(null) + + const onEnterView = useCallback(() => { + if (player.status === 'readyToPlay') { + player.play() + } + }, [player]) + + const onLeaveView = useCallback(() => { + player.pause() + }, [player]) + + const enterFullscreen = useCallback(() => { + if (ref.current) { + ref.current.enterFullscreen() + } + }, []) + + useFrameCallback(() => { + const measurement = measure(aref) + + if (measurement) { + if (hasLeftView.value) { + // Check if the video is in view + if ( + measurement.pageY >= 0 && + measurement.pageY + measurement.height <= windowHeight + ) { + runOnJS(onEnterView)() + hasLeftView.value = false + } + } else { + // Check if the video is out of view + if ( + measurement.pageY + measurement.height < 0 || + measurement.pageY > windowHeight + ) { + runOnJS(onLeaveView)() + hasLeftView.value = true + } + } + } + }) + + return ( + <Animated.View + style={[a.flex_1, a.relative]} + ref={aref} + collapsable={false}> + <VideoView + ref={ref} + player={player} + style={a.flex_1} + nativeControls={true} + /> + <VideoControls player={player} enterFullscreen={enterFullscreen} /> + </Animated.View> + ) +} + +function VideoControls({ + player, + enterFullscreen, +}: { + player: VideoPlayer + enterFullscreen: () => void +}) { + const [currentTime, setCurrentTime] = useState(Math.floor(player.currentTime)) + + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(Math.floor(player.duration - player.currentTime)) + // how often should we update the time? + // 1000 gets out of sync with the video time + }, 250) + + return () => { + clearInterval(interval) + } + }, [player]) + + const minutes = Math.floor(currentTime / 60) + const seconds = String(currentTime % 60).padStart(2, '0') + + return ( + <View style={[a.absolute, a.inset_0]}> + <View style={styles.timeContainer} pointerEvents="none"> + <Text style={styles.timeElapsed}> + {minutes}:{seconds} + </Text> + </View> + <Pressable + onPress={enterFullscreen} + style={a.flex_1} + accessibilityLabel="Video" + accessibilityHint="Tap to enter full screen" + accessibilityRole="button" + /> + </View> + ) +} + +const styles = StyleSheet.create({ + timeContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: 6, + paddingVertical: 3, + position: 'absolute', + left: 5, + bottom: 5, + }, + timeElapsed: { + color: 'white', + fontSize: 12, + fontWeight: 'bold', + }, +}) diff --git a/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx b/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx new file mode 100644 index 000000000..cb02743c6 --- /dev/null +++ b/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx @@ -0,0 +1,52 @@ +import React, {useEffect, useRef} from 'react' +import Hls from 'hls.js' + +import {atoms as a} from '#/alf' + +export const VideoEmbedInner = ({source}: {source: string}) => { + const ref = useRef<HTMLVideoElement>(null) + + // Use HLS.js to play HLS video + useEffect(() => { + if (ref.current) { + if (ref.current.canPlayType('application/vnd.apple.mpegurl')) { + ref.current.src = source + } else if (Hls.isSupported()) { + var hls = new Hls() + hls.loadSource(source) + hls.attachMedia(ref.current) + } else { + // TODO: fallback + } + } + }, [source]) + + useEffect(() => { + if (ref.current) { + const observer = new IntersectionObserver( + ([entry]) => { + if (ref.current) { + if (entry.isIntersecting) { + if (ref.current.paused) { + ref.current.play() + } + } else { + if (!ref.current.paused) { + ref.current.pause() + } + } + } + }, + {threshold: 0}, + ) + + observer.observe(ref.current) + + return () => { + observer.disconnect() + } + } + }, []) + + return <video ref={ref} style={a.flex_1} controls playsInline autoPlay loop /> +} diff --git a/src/view/com/util/post-embeds/VideoPlayerContext.tsx b/src/view/com/util/post-embeds/VideoPlayerContext.tsx new file mode 100644 index 000000000..bc5d9d370 --- /dev/null +++ b/src/view/com/util/post-embeds/VideoPlayerContext.tsx @@ -0,0 +1,41 @@ +import React, {useContext, useEffect} from 'react' +import type {VideoPlayer} from 'expo-video' +import {useVideoPlayer as useExpoVideoPlayer} from 'expo-video' + +const VideoPlayerContext = React.createContext<VideoPlayer | null>(null) + +export function VideoPlayerProvider({ + viewId, + source, + children, +}: { + viewId: string | null + source: string + children: React.ReactNode +}) { + // eslint-disable-next-line @typescript-eslint/no-shadow + const player = useExpoVideoPlayer(source, player => { + player.loop = true + player.play() + }) + + // make sure we're playing every time the viewId changes + // this means the video is different + useEffect(() => { + player.play() + }, [viewId, player]) + + return ( + <VideoPlayerContext.Provider value={player}> + {children} + </VideoPlayerContext.Provider> + ) +} + +export function useVideoPlayer() { + const context = useContext(VideoPlayerContext) + if (!context) { + throw new Error('useVideoPlayer must be used within a VideoPlayerProvider') + } + return context +} diff --git a/src/view/com/util/post-embeds/VideoPlayerContext.web.tsx b/src/view/com/util/post-embeds/VideoPlayerContext.web.tsx new file mode 100644 index 000000000..329fb1206 --- /dev/null +++ b/src/view/com/util/post-embeds/VideoPlayerContext.web.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export function VideoPlayerProvider({children}: {children: React.ReactNode}) { + return children +} + +export function useVideoPlayer() { + throw new Error('useVideoPlayer must not be used on web') +} |