* 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 <me@haileyok.com>zio/stable
parent
4ec999cab7
commit
00240b95b9
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="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"/></svg>
|
After Width: | Height: | Size: 239 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 374 B |
|
@ -143,6 +143,7 @@
|
||||||
"expo-web-browser": "~13.0.3",
|
"expo-web-browser": "~13.0.3",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
|
"hls.js": "^1.5.11",
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lande": "^1.0.10",
|
"lande": "^1.0.10",
|
||||||
|
|
|
@ -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<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||||
|
+ val fullscreenButton =
|
||||||
|
+ findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||||
|
fullscreenButton?.visibility = if (visible) {
|
||||||
|
android.view.View.VISIBLE
|
||||||
|
} else {
|
|
@ -23,10 +23,12 @@ import {
|
||||||
} from '#/lib/statsig/statsig'
|
} from '#/lib/statsig/statsig'
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||||
|
import I18nProvider from '#/locale/i18nProvider'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {Provider as A11yProvider} from '#/state/a11y'
|
import {Provider as A11yProvider} from '#/state/a11y'
|
||||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||||
|
import {listenSessionDropped} from '#/state/events'
|
||||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||||
import {MessagesProvider} from '#/state/messages'
|
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 SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
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 * as Toast from '#/view/com/util/Toast'
|
||||||
import {Shell} from '#/view/shell'
|
import {Shell} from '#/view/shell'
|
||||||
import {ThemeProvider as Alf} from '#/alf'
|
import {ThemeProvider as Alf} from '#/alf'
|
||||||
|
@ -58,8 +61,6 @@ import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
import {Provider as TourProvider} from '#/tours'
|
import {Provider as TourProvider} from '#/tours'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import I18nProvider from './locale/i18nProvider'
|
|
||||||
import {listenSessionDropped} from './state/events'
|
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
|
@ -107,42 +108,44 @@ function InnerApp() {
|
||||||
<Alf theme={theme}>
|
<Alf theme={theme}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||||
<RootSiblingParent>
|
<ActiveVideoProvider>
|
||||||
<React.Fragment
|
<RootSiblingParent>
|
||||||
// Resets the entire tree below when it changes:
|
<React.Fragment
|
||||||
key={currentAccount?.did}>
|
// Resets the entire tree below when it changes:
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
key={currentAccount?.did}>
|
||||||
<StatsigProvider>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<MessagesProvider>
|
<StatsigProvider>
|
||||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
<MessagesProvider>
|
||||||
<LabelDefsProvider>
|
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||||
<ModerationOptsProvider>
|
<LabelDefsProvider>
|
||||||
<LoggedOutViewProvider>
|
<ModerationOptsProvider>
|
||||||
<SelectedFeedProvider>
|
<LoggedOutViewProvider>
|
||||||
<UnreadNotifsProvider>
|
<SelectedFeedProvider>
|
||||||
<BackgroundNotificationPreferencesProvider>
|
<UnreadNotifsProvider>
|
||||||
<MutedThreadsProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<TourProvider>
|
<MutedThreadsProvider>
|
||||||
<ProgressGuideProvider>
|
<TourProvider>
|
||||||
<GestureHandlerRootView
|
<ProgressGuideProvider>
|
||||||
style={s.h100pct}>
|
<GestureHandlerRootView
|
||||||
<TestCtrls />
|
style={s.h100pct}>
|
||||||
<Shell />
|
<TestCtrls />
|
||||||
</GestureHandlerRootView>
|
<Shell />
|
||||||
</ProgressGuideProvider>
|
</GestureHandlerRootView>
|
||||||
</TourProvider>
|
</ProgressGuideProvider>
|
||||||
</MutedThreadsProvider>
|
</TourProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</MutedThreadsProvider>
|
||||||
</UnreadNotifsProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</SelectedFeedProvider>
|
</UnreadNotifsProvider>
|
||||||
</LoggedOutViewProvider>
|
</SelectedFeedProvider>
|
||||||
</ModerationOptsProvider>
|
</LoggedOutViewProvider>
|
||||||
</LabelDefsProvider>
|
</ModerationOptsProvider>
|
||||||
</MessagesProvider>
|
</LabelDefsProvider>
|
||||||
</StatsigProvider>
|
</MessagesProvider>
|
||||||
</QueryProvider>
|
</StatsigProvider>
|
||||||
</React.Fragment>
|
</QueryProvider>
|
||||||
</RootSiblingParent>
|
</React.Fragment>
|
||||||
|
</RootSiblingParent>
|
||||||
|
</ActiveVideoProvider>
|
||||||
</Splash>
|
</Splash>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Alf>
|
</Alf>
|
||||||
|
|
|
@ -12,10 +12,12 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
|
||||||
import {QueryProvider} from '#/lib/react-query'
|
import {QueryProvider} from '#/lib/react-query'
|
||||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||||
|
import I18nProvider from '#/locale/i18nProvider'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {Provider as A11yProvider} from '#/state/a11y'
|
import {Provider as A11yProvider} from '#/state/a11y'
|
||||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||||
|
import {listenSessionDropped} from '#/state/events'
|
||||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||||
import {MessagesProvider} from '#/state/messages'
|
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 ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
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 * as Toast from '#/view/com/util/Toast'
|
||||||
import {ToastContainer} from '#/view/com/util/Toast.web'
|
import {ToastContainer} from '#/view/com/util/Toast.web'
|
||||||
import {Shell} from '#/view/shell/index'
|
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 PortalProvider} from '#/components/Portal'
|
||||||
import {Provider as TourProvider} from '#/tours'
|
import {Provider as TourProvider} from '#/tours'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import I18nProvider from './locale/i18nProvider'
|
|
||||||
import {listenSessionDropped} from './state/events'
|
|
||||||
|
|
||||||
function InnerApp() {
|
function InnerApp() {
|
||||||
const [isReady, setIsReady] = React.useState(false)
|
const [isReady, setIsReady] = React.useState(false)
|
||||||
|
@ -92,39 +93,41 @@ function InnerApp() {
|
||||||
<Alf theme={theme}>
|
<Alf theme={theme}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<RootSiblingParent>
|
<RootSiblingParent>
|
||||||
<React.Fragment
|
<ActiveVideoProvider>
|
||||||
// Resets the entire tree below when it changes:
|
<React.Fragment
|
||||||
key={currentAccount?.did}>
|
// Resets the entire tree below when it changes:
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
key={currentAccount?.did}>
|
||||||
<StatsigProvider>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<MessagesProvider>
|
<StatsigProvider>
|
||||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
<MessagesProvider>
|
||||||
<LabelDefsProvider>
|
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||||
<ModerationOptsProvider>
|
<LabelDefsProvider>
|
||||||
<LoggedOutViewProvider>
|
<ModerationOptsProvider>
|
||||||
<SelectedFeedProvider>
|
<LoggedOutViewProvider>
|
||||||
<UnreadNotifsProvider>
|
<SelectedFeedProvider>
|
||||||
<BackgroundNotificationPreferencesProvider>
|
<UnreadNotifsProvider>
|
||||||
<MutedThreadsProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<SafeAreaProvider>
|
<MutedThreadsProvider>
|
||||||
<TourProvider>
|
<SafeAreaProvider>
|
||||||
<ProgressGuideProvider>
|
<TourProvider>
|
||||||
<Shell />
|
<ProgressGuideProvider>
|
||||||
</ProgressGuideProvider>
|
<Shell />
|
||||||
</TourProvider>
|
</ProgressGuideProvider>
|
||||||
</SafeAreaProvider>
|
</TourProvider>
|
||||||
</MutedThreadsProvider>
|
</SafeAreaProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</MutedThreadsProvider>
|
||||||
</UnreadNotifsProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</SelectedFeedProvider>
|
</UnreadNotifsProvider>
|
||||||
</LoggedOutViewProvider>
|
</SelectedFeedProvider>
|
||||||
</ModerationOptsProvider>
|
</LoggedOutViewProvider>
|
||||||
</LabelDefsProvider>
|
</ModerationOptsProvider>
|
||||||
</MessagesProvider>
|
</LabelDefsProvider>
|
||||||
</StatsigProvider>
|
</MessagesProvider>
|
||||||
</QueryProvider>
|
</StatsigProvider>
|
||||||
</React.Fragment>
|
</QueryProvider>
|
||||||
<ToastContainer />
|
</React.Fragment>
|
||||||
|
<ToastContainer />
|
||||||
|
</ActiveVideoProvider>
|
||||||
</RootSiblingParent>
|
</RootSiblingParent>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Alf>
|
</Alf>
|
||||||
|
|
|
@ -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',
|
||||||
|
})
|
|
@ -210,38 +210,40 @@ function PostInner({
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<LabelsOnMyPost post={post} />
|
<LabelsOnMyPost post={post} />
|
||||||
<ContentHider
|
{false && (
|
||||||
modui={moderation.ui('contentView')}
|
<ContentHider
|
||||||
style={styles.contentHider}
|
|
||||||
childContainerStyle={styles.contentHiderChild}>
|
|
||||||
<PostAlerts
|
|
||||||
modui={moderation.ui('contentView')}
|
modui={moderation.ui('contentView')}
|
||||||
style={[a.py_xs]}
|
style={styles.contentHider}
|
||||||
/>
|
childContainerStyle={styles.contentHiderChild}>
|
||||||
{richText.text ? (
|
<PostAlerts
|
||||||
<View style={styles.postTextContainer}>
|
modui={moderation.ui('contentView')}
|
||||||
<RichText
|
style={[a.py_xs]}
|
||||||
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}
|
{richText.text ? (
|
||||||
{post.embed ? (
|
<View style={styles.postTextContainer}>
|
||||||
<PostEmbeds embed={post.embed} moderation={moderation} />
|
<RichText
|
||||||
) : null}
|
enableTags
|
||||||
</ContentHider>
|
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
|
<PostCtrls
|
||||||
post={post}
|
post={post}
|
||||||
record={record}
|
record={record}
|
||||||
|
|
|
@ -16,8 +16,10 @@ import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types'
|
import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types'
|
||||||
import {MAX_POST_LINES} from 'lib/constants'
|
import {MAX_POST_LINES} from 'lib/constants'
|
||||||
|
@ -29,6 +31,7 @@ import {countLines} from 'lib/strings/helpers'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {precacheProfile} from 'state/queries/profile'
|
import {precacheProfile} from 'state/queries/profile'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
|
@ -38,13 +41,12 @@ import {FeedNameText} from '../util/FeedInfoText'
|
||||||
import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link'
|
import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link'
|
||||||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||||
import {PostEmbeds} from '../util/post-embeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
|
import {VideoEmbed} from '../util/post-embeds/VideoEmbed'
|
||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||||
import {AviFollowButton} from './AviFollowButton'
|
import {AviFollowButton} from './AviFollowButton'
|
||||||
import hairlineWidth = StyleSheet.hairlineWidth
|
import hairlineWidth = StyleSheet.hairlineWidth
|
||||||
import {useSession} from '#/state/session'
|
|
||||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
|
||||||
|
|
||||||
interface FeedItemProps {
|
interface FeedItemProps {
|
||||||
record: AppBskyFeedPost.Record
|
record: AppBskyFeedPost.Record
|
||||||
|
@ -136,6 +138,8 @@ let FeedItemInner = ({
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const gate = useGate()
|
||||||
|
|
||||||
const href = useMemo(() => {
|
const href = useMemo(() => {
|
||||||
const urip = new AtUri(post.uri)
|
const urip = new AtUri(post.uri)
|
||||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||||
|
@ -354,6 +358,9 @@ let FeedItemInner = ({
|
||||||
postAuthor={post.author}
|
postAuthor={post.author}
|
||||||
onOpenEmbed={onOpenEmbed}
|
onOpenEmbed={onOpenEmbed}
|
||||||
/>
|
/>
|
||||||
|
{__DEV__ && gate('videos') && (
|
||||||
|
<VideoEmbed source="https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8" />
|
||||||
|
)}
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
post={post}
|
post={post}
|
||||||
record={record}
|
record={record}
|
||||||
|
|
|
@ -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],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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 />
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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')
|
||||||
|
}
|
|
@ -13345,6 +13345,11 @@ history@^5.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.7.6"
|
"@babel/runtime" "^7.7.6"
|
||||||
|
|
||||||
|
hls.js@^1.5.11:
|
||||||
|
version "1.5.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.5.11.tgz#3941347df454983859ae8c75fe19e8818719a826"
|
||||||
|
integrity sha512-q3We1izi2+qkOO+TvZdHv+dx6aFzdtk3xc1/Qesrvto4thLTT/x/1FK85c5h1qZE4MmMBNgKg+MIW8nxQfxwBw==
|
||||||
|
|
||||||
hmac-drbg@^1.0.1:
|
hmac-drbg@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||||
|
|
Loading…
Reference in New Issue