From fac1af43b0300e9d9be16641afc4fbeef0a91ccf Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 25 Jul 2024 13:16:21 -0500 Subject: [PATCH 01/15] Fuggedaboudit (#4829) --- src/view/com/notifications/FeedItem.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index d31962ff35..3171f88dbc 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -171,7 +171,6 @@ let FeedItem = ({ ) } - let isFollowBack = false let action = '' let icon = ( From 4291711f1d4bf3921b1e805d7726b0764757f257 Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 25 Jul 2024 19:53:12 +0100 Subject: [PATCH 02/15] Fix sloppy filter(Boolean) types (#4830) * Fix sloppy filter(Boolean) in threadgate * Fix sloppy filter(Boolean) in Explore * Fix sloppy filter(Boolean) in post-feed * Harden FeedPostSliceItem.reason type def * Harden parentAuthor types * Fix lying component types, handle blocks --- src/state/queries/post-feed.ts | 29 ++++++++--- src/state/queries/threadgate.ts | 22 ++++----- src/view/com/posts/FeedItem.tsx | 76 ++++++++++++++++++----------- src/view/screens/Search/Explore.tsx | 17 ++++--- 4 files changed, 88 insertions(+), 56 deletions(-) diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 315c9cfadd..c1484a59e7 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -78,7 +78,10 @@ export interface FeedPostSliceItem { uri: string post: AppBskyFeedDefs.PostView record: AppBskyFeedPost.Record - reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource + reason?: + | AppBskyFeedDefs.ReasonRepost + | ReasonFeedSource + | {[k: string]: unknown; $type: string} feedContext: string | undefined moderation: ModerationDecision parentAuthor?: AppBskyActorDefs.ProfileViewBasic @@ -323,7 +326,7 @@ export function usePostFeedQuery( ) } - return { + const feedPostSlice: FeedPostSlice = { _reactKey: slice._reactKey, _isFeedPostSlice: true, rootUri: slice.rootItem.post.uri, @@ -341,15 +344,23 @@ export function usePostFeedQuery( AppBskyFeedPost.validateRecord(item.post.record) .success ) { - const parentAuthor = - item.reply?.parent?.author ?? - slice.items[i + 1]?.reply?.grandparentAuthor + const parent = item.reply?.parent + let parentAuthor: + | AppBskyActorDefs.ProfileViewBasic + | undefined + if (AppBskyFeedDefs.isPostView(parent)) { + parentAuthor = parent.author + } + if (!parentAuthor) { + parentAuthor = + slice.items[i + 1]?.reply?.grandparentAuthor + } const replyRef = item.reply const isParentBlocked = AppBskyFeedDefs.isBlockedPost( replyRef?.parent, ) - return { + const feedPostSliceItem: FeedPostSliceItem = { _reactKey: `${slice._reactKey}-${i}-${item.post.uri}`, uri: item.post.uri, post: item.post, @@ -363,13 +374,15 @@ export function usePostFeedQuery( parentAuthor, isParentBlocked, } + return feedPostSliceItem } return undefined }) - .filter(Boolean) as FeedPostSliceItem[], + .filter((n?: T): n is T => Boolean(n)), } + return feedPostSlice }) - .filter(Boolean) as FeedPostSlice[], + .filter((n?: T): n is T => Boolean(n)), })), ], } diff --git a/src/state/queries/threadgate.ts b/src/state/queries/threadgate.ts index 67c6f8c084..c05d1f5644 100644 --- a/src/state/queries/threadgate.ts +++ b/src/state/queries/threadgate.ts @@ -4,7 +4,7 @@ export type ThreadgateSetting = | {type: 'nobody'} | {type: 'mention'} | {type: 'following'} - | {type: 'list'; list: string} + | {type: 'list'; list: unknown} export function threadgateViewToSettings( threadgate: AppBskyFeedDefs.ThreadgateView | undefined, @@ -21,18 +21,18 @@ export function threadgateViewToSettings( if (!record.allow?.length) { return [{type: 'nobody'}] } - return record.allow + const settings: ThreadgateSetting[] = record.allow .map(allow => { + let setting: ThreadgateSetting | undefined if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') { - return {type: 'mention'} + setting = {type: 'mention'} + } else if (allow.$type === 'app.bsky.feed.threadgate#followingRule') { + setting = {type: 'following'} + } else if (allow.$type === 'app.bsky.feed.threadgate#listRule') { + setting = {type: 'list', list: allow.list} } - if (allow.$type === 'app.bsky.feed.threadgate#followingRule') { - return {type: 'following'} - } - if (allow.$type === 'app.bsky.feed.threadgate#listRule') { - return {type: 'list', list: allow.list} - } - return undefined + return setting }) - .filter(Boolean) as ThreadgateSetting[] + .filter((n?: T): n is T => Boolean(n)) + return settings } diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index a59eeea52e..dbc5796db5 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -48,7 +48,11 @@ import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repos interface FeedItemProps { record: AppBskyFeedPost.Record - reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined + reason: + | AppBskyFeedDefs.ReasonRepost + | ReasonFeedSource + | {[k: string]: unknown; $type: string} + | undefined moderation: ModerationDecision parentAuthor: AppBskyActorDefs.ProfileViewBasic | undefined showReplyTo: boolean @@ -337,9 +341,11 @@ let FeedItemInner = ({ postHref={href} onOpenAuthor={onOpenAuthor} /> - {!isThreadChild && showReplyTo && parentAuthor && ( - - )} + {!isThreadChild && + showReplyTo && + (parentAuthor || isParentBlocked) && ( + + )} Reply to a blocked post + } else if (profile != null) { + const isMe = profile.did === currentAccount?.did + if (isMe) { + label = Reply to you + } else { + label = ( + + Reply to{' '} + + + + + ) + } + } + + if (!label) { + // Should not happen. + return null + } return ( @@ -450,29 +490,7 @@ function ReplyToLabel({ style={[pal.textLight, s.mr2]} lineHeight={1.2} numberOfLines={1}> - {isMe ? ( - Reply to you - ) : blocked ? ( - Reply to a blocked post - ) : ( - - Reply to{' '} - - - - - )} + {label} ) diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index 05fd85effe..e9b7445276 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -75,17 +75,17 @@ function SuggestedItemsHeader({ ) } -type LoadMoreItems = +type LoadMoreItem = | { type: 'profile' key: string - avatar: string + avatar: string | undefined moderation: ModerationDecision } | { type: 'feed' key: string - avatar: string + avatar: string | undefined moderation: undefined } @@ -98,27 +98,28 @@ function LoadMore({ }) { const t = useTheme() const {_} = useLingui() - const items = React.useMemo(() => { + const items: LoadMoreItem[] = React.useMemo(() => { return item.items .map(_item => { + let loadMoreItem: LoadMoreItem | undefined if (_item.type === 'profile') { - return { + loadMoreItem = { type: 'profile', key: _item.profile.did, avatar: _item.profile.avatar, moderation: moderateProfile(_item.profile, moderationOpts!), } } else if (_item.type === 'feed') { - return { + loadMoreItem = { type: 'feed', key: _item.feed.uri, avatar: _item.feed.avatar, moderation: undefined, } } - return undefined + return loadMoreItem }) - .filter(Boolean) as LoadMoreItems[] + .filter((n?: T): n is T => Boolean(n)) }, [item.items, moderationOpts]) if (items.length === 0) return null From 4ec999cab7104a381c8c7a3202ebb2d01599a513 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 25 Jul 2024 12:41:32 -0700 Subject: [PATCH 03/15] Bump 1.90 (#4832) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 51177943bb..be22209b0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.89.0", + "version": "1.90.0", "private": true, "engines": { "node": ">=18" From 00240b95b90847f6691f7fa19c19f37d2ffc6624 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 25 Jul 2024 20:41:50 +0100 Subject: [PATCH 04/15] [Videos] Video player - PR #1 - basic player (#4731) * add ffmpeg-kit-react-native * get select video button + compression working * up res to 1080p * add progress component * move logic out of compressVideo * (WIP) add lonestar compression * rework web compression a bit * mess around with adding a thumbnail * 3mbps * replace * use 3mbps * add expo-video * remove unnecessary try/catch * rm ToastAndroid * fix web * wrap lazy component in suspense * gate video select button * rm web compression * flip sign * remove expo-video from web * review nits * add video picker permissions + rm temp buttons * add ffmpeg-kit-react-native * replace * hls-capable player * start trying to hoist up video player instance * hoist video player and move things around * always show native controls * fix controls on expo video android * gate temp video player in feed * rm IS_DEV, doesn't do what I thought it did * use __DEV__ instead --------- Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com> Co-authored-by: Hailey --- assets/icons/play_filled_corner2_rounded.svg | 1 + assets/icons/play_stroke2_corner2_rounded.svg | 1 + package.json | 1 + patches/expo-video+1.1.10.patch | 20 +++ src/App.native.tsx | 79 +++++----- src/App.web.tsx | 73 ++++----- src/components/icons/Play.tsx | 9 ++ src/view/com/post/Post.tsx | 62 ++++---- src/view/com/posts/FeedItem.tsx | 11 +- .../util/post-embeds/ActiveVideoContext.tsx | 48 ++++++ src/view/com/util/post-embeds/VideoEmbed.tsx | 44 ++++++ .../com/util/post-embeds/VideoEmbedInner.tsx | 138 ++++++++++++++++++ .../util/post-embeds/VideoEmbedInner.web.tsx | 52 +++++++ .../util/post-embeds/VideoPlayerContext.tsx | 41 ++++++ .../post-embeds/VideoPlayerContext.web.tsx | 9 ++ yarn.lock | 5 + 16 files changed, 489 insertions(+), 105 deletions(-) create mode 100644 assets/icons/play_filled_corner2_rounded.svg create mode 100644 assets/icons/play_stroke2_corner2_rounded.svg create mode 100644 patches/expo-video+1.1.10.patch create mode 100644 src/components/icons/Play.tsx create mode 100644 src/view/com/util/post-embeds/ActiveVideoContext.tsx create mode 100644 src/view/com/util/post-embeds/VideoEmbed.tsx create mode 100644 src/view/com/util/post-embeds/VideoEmbedInner.tsx create mode 100644 src/view/com/util/post-embeds/VideoEmbedInner.web.tsx create mode 100644 src/view/com/util/post-embeds/VideoPlayerContext.tsx create mode 100644 src/view/com/util/post-embeds/VideoPlayerContext.web.tsx diff --git a/assets/icons/play_filled_corner2_rounded.svg b/assets/icons/play_filled_corner2_rounded.svg new file mode 100644 index 0000000000..e25e8d4628 --- /dev/null +++ b/assets/icons/play_filled_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/play_stroke2_corner2_rounded.svg b/assets/icons/play_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..54bba91fb9 --- /dev/null +++ b/assets/icons/play_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/package.json b/package.json index be22209b0d..091fb2fd70 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "expo-web-browser": "~13.0.3", "fast-text-encoding": "^1.0.6", "history": "^5.3.0", + "hls.js": "^1.5.11", "js-sha256": "^0.9.0", "jwt-decode": "^4.0.0", "lande": "^1.0.10", diff --git a/patches/expo-video+1.1.10.patch b/patches/expo-video+1.1.10.patch new file mode 100644 index 0000000000..b183be9d41 --- /dev/null +++ b/patches/expo-video+1.1.10.patch @@ -0,0 +1,20 @@ +--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt ++++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole + setShowPreviousButton(!requireLinearPlayback) + setShowNextButton(!requireLinearPlayback) + setTimeBarInteractive(requireLinearPlayback) ++ setShowSubtitleButton(true) + } + + @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) { + + @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) + internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) { +- val fullscreenButton = findViewById(androidx.media3.ui.R.id.exo_fullscreen) ++ val fullscreenButton = ++ findViewById(androidx.media3.ui.R.id.exo_fullscreen) + fullscreenButton?.visibility = if (visible) { + android.view.View.VISIBLE + } else { diff --git a/src/App.native.tsx b/src/App.native.tsx index ed76c753b5..d2c20fc8e7 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() { - - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index a64988f380..df6fbf2449 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() { - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/icons/Play.tsx b/src/components/icons/Play.tsx new file mode 100644 index 0000000000..acf421d57c --- /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 a05339d4dc..425a2257f6 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -210,38 +210,40 @@ function PostInner({ )} - - - {richText.text ? ( - - - - ) : undefined} - {limitLines ? ( - + - ) : undefined} - {post.embed ? ( - - ) : null} - + {richText.text ? ( + + + + ) : undefined} + {limitLines ? ( + + ) : undefined} + {post.embed ? ( + + ) : null} + + )} { 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') && ( + + )} void +} | null>(null) + +export function ActiveVideoProvider({children}: {children: React.ReactNode}) { + const [activeViewId, setActiveViewId] = useState(null) + const [source, setSource] = useState(null) + + const value = useMemo( + () => ({ + activeViewId, + setActiveView: (viewId: string, src: string) => { + setActiveViewId(viewId) + setSource(src) + }, + }), + [activeViewId], + ) + + return ( + + + {children} + + + ) +} + +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 0000000000..5e5293a553 --- /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 ( + + {active ? ( + + ) : ( + + )} + + ) +} 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 0000000000..ef06787097 --- /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() + const {height: windowHeight} = useWindowDimensions() + const hasLeftView = useSharedValue(false) + const ref = useRef(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 ( + + + + + ) +} + +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 ( + + + + {minutes}:{seconds} + + + + + ) +} + +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 0000000000..cb02743c6f --- /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(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