Merge remote-tracking branch 'origin/main' into Improve-style-consistency

This commit is contained in:
Hailey
2024-08-11 12:47:19 -07:00
20 changed files with 537 additions and 133 deletions
+3
View File
@@ -61,6 +61,7 @@ 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 {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army'
SplashScreen.preventAutoHideAsync()
@@ -157,6 +158,8 @@ function App() {
const [isReady, setReady] = useState(false)
React.useEffect(() => {
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
PlatformInfo.setAudioActive(true)
initPersistedState().then(() => setReady(true))
}, [])
+1 -1
View File
@@ -249,7 +249,7 @@ function DialogError({details}: {details?: string}) {
const control = Dialog.useDialogContext()
return (
<Dialog.ScrollableInner style={a.gap_md} label={_(msg`An error occured`)}>
<Dialog.ScrollableInner style={a.gap_md} label={_(msg`An error has occurred`)}>
<Dialog.Close />
<ErrorScreen
title={_(msg`Oh no!`)}
@@ -3,6 +3,7 @@ import {View} from 'react-native'
// @ts-expect-error no type definition
import ProgressPie from 'react-native-progress/Pie'
import {ImagePickerAsset} from 'expo-image-picker'
import {Trans} from '@lingui/macro'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
@@ -46,7 +47,9 @@ export function VideoTranscodeProgress({
color={t.atoms.text.color}
progress={progress}
/>
<Text>Compressing...</Text>
<Text>
<Trans>Compressing...</Trans>
</Text>
</View>
</View>
)
+15 -5
View File
@@ -1,4 +1,4 @@
import React, {memo, useMemo, useState} from 'react'
import React, {memo, useId, useMemo, useState} from 'react'
import {StyleSheet, View} from 'react-native'
import {
AppBskyActorDefs,
@@ -137,7 +137,6 @@ let FeedItemInner = ({
const {openComposer} = useComposerControls()
const pal = usePalette('default')
const {_} = useLingui()
const gate = useGate()
const href = useMemo(() => {
const urip = new AtUri(post.uri)
@@ -356,9 +355,7 @@ let FeedItemInner = ({
postAuthor={post.author}
onOpenEmbed={onOpenEmbed}
/>
{gate('video_debug') && (
<VideoEmbed source="https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8" />
)}
<VideoDebug />
<PostCtrls
post={post}
record={record}
@@ -501,6 +498,19 @@ function ReplyToLabel({
)
}
function VideoDebug() {
const gate = useGate()
const id = useId()
if (!gate('video_debug')) return null
return (
<VideoEmbed
source={`https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8?ignore_me_just_testing_frontend_stuff=${id}`}
/>
)
}
const styles = StyleSheet.create({
outer: {
paddingLeft: 10,
+48 -23
View File
@@ -1,6 +1,6 @@
import React from 'react'
import React, {useCallback, useState} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {VideoEmbedInnerNative} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
@@ -8,13 +8,23 @@ 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 {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
import {ErrorBoundary} from '../ErrorBoundary'
import {useActiveVideoView} from './ActiveVideoContext'
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
export function VideoEmbed({source}: {source: string}) {
const t = useTheme()
const {active, setActive} = useActiveVideoView({source})
const {_} = useLingui()
const [key, setKey] = useState(0)
const renderError = useCallback(
(error: unknown) => (
<VideoError error={error} retry={() => setKey(key + 1)} />
),
[key],
)
return (
<View
style={[
@@ -25,27 +35,42 @@ export function VideoEmbed({source}: {source: string}) {
t.atoms.bg_contrast_25,
a.my_xs,
]}>
<VisibilityView
enabled={true}
onChangeStatus={isActive => {
if (isActive) {
setActive()
}
}}>
{active ? (
<VideoEmbedInnerNative />
) : (
<Button
style={[a.flex_1, t.atoms.bg_contrast_25]}
onPress={setActive}
label={_(msg`Play video`)}
variant="ghost"
color="secondary"
size="large">
<ButtonIcon icon={PlayIcon} />
</Button>
)}
</VisibilityView>
<ErrorBoundary renderError={renderError} key={key}>
<VisibilityView
enabled={true}
onChangeStatus={isActive => {
if (isActive) {
setActive()
}
}}>
{active ? (
<VideoEmbedInnerNative />
) : (
<Button
style={[a.flex_1, t.atoms.bg_contrast_25]}
onPress={setActive}
label={_(msg`Play video`)}
variant="ghost"
color="secondary"
size="large">
<ButtonIcon icon={PlayIcon} />
</Button>
)}
</VisibilityView>
</ErrorBoundary>
</View>
)
}
function VideoError({retry}: {error: unknown; retry: () => void}) {
return (
<VideoFallback.Container>
<VideoFallback.Text>
<Trans>
An error occurred while loading the video. Please try again later.
</Trans>
</VideoFallback.Text>
<VideoFallback.RetryButton onPress={retry} />
</VideoFallback.Container>
)
}
@@ -1,17 +1,15 @@
import React, {useCallback, useEffect, useRef, useState} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/macro'
import {
HLSUnsupportedError,
VideoEmbedInnerWeb,
} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
import {ErrorBoundary} from '../ErrorBoundary'
import {useActiveVideoView} from './ActiveVideoContext'
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
export function VideoEmbed({source}: {source: string}) {
const t = useTheme()
@@ -138,32 +136,11 @@ function ViewportObserver({
}
function VideoError({error, retry}: {error: unknown; retry: () => void}) {
const t = useTheme()
const {_} = useLingui()
const isHLS = error instanceof HLSUnsupportedError
return (
<View
style={[
a.flex_1,
t.atoms.bg_contrast_25,
a.justify_center,
a.align_center,
a.px_lg,
a.border,
t.atoms.border_contrast_low,
a.rounded_sm,
a.gap_lg,
]}>
<Text
style={[
a.text_center,
t.atoms.text_contrast_high,
a.text_md,
a.leading_snug,
{maxWidth: 300},
]}>
<VideoFallback.Container>
<VideoFallback.Text>
{isHLS ? (
<Trans>
Your browser does not support the video format. Please try a
@@ -174,19 +151,8 @@ function VideoError({error, retry}: {error: unknown; retry: () => void}) {
An error occurred while loading the video. Please try again later.
</Trans>
)}
</Text>
{!isHLS && (
<Button
onPress={retry}
size="small"
color="secondary_inverted"
variant="solid"
label={_(msg`Retry`)}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
)}
</View>
</VideoFallback.Text>
{!isHLS && <VideoFallback.RetryButton onPress={retry} />}
</VideoFallback.Container>
)
}
@@ -1,27 +1,76 @@
import React, {useEffect, useRef, useState} from 'react'
import React, {useCallback, useEffect, useRef, useState} from 'react'
import {Pressable, View} from 'react-native'
import Animated, {FadeInDown, FadeOutDown} from 'react-native-reanimated'
import {VideoPlayer, VideoView} from 'expo-video'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useIsFocused} from '@react-navigation/native'
import {useVideoPlayer} from 'view/com/util/post-embeds/VideoPlayerContext'
import {android, atoms as a} from '#/alf'
import {HITSLOP_30} from '#/lib/constants'
import {useAppState} from '#/lib/hooks/useAppState'
import {useVideoPlayer} from '#/view/com/util/post-embeds/VideoPlayerContext'
import {android, atoms as a, useTheme} from '#/alf'
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {Text} from '#/components/Typography'
import {
AudioCategory,
PlatformInfo,
} from '../../../../../../modules/expo-bluesky-swiss-army'
export function VideoEmbedInnerNative() {
const player = useVideoPlayer()
const ref = useRef<VideoView>(null)
const isScreenFocused = useIsFocused()
const isAppFocused = useAppState()
const prevFocusedRef = useRef(isAppFocused)
// resume video when coming back from background
useEffect(() => {
if (isAppFocused !== prevFocusedRef.current) {
prevFocusedRef.current = isAppFocused
if (isAppFocused === 'active') {
player.play()
}
}
}, [isAppFocused, player])
// pause the video when the screen is not focused
useEffect(() => {
if (!isScreenFocused) {
let wasPlaying = player.playing
player.pause()
return () => {
if (wasPlaying) player.play()
}
}
}, [isScreenFocused, player])
const enterFullscreen = useCallback(() => {
ref.current?.enterFullscreen()
}, [])
return (
<View style={[a.flex_1, a.relative]} collapsable={false}>
<View style={[a.flex_1, a.relative]}>
<VideoView
ref={ref}
player={player}
style={a.flex_1}
style={[a.flex_1, a.rounded_sm]}
nativeControls={true}
onEnterFullscreen={() => {
PlatformInfo.setAudioCategory(AudioCategory.Playback)
PlatformInfo.setAudioActive(false)
player.muted = false
}}
onExitFullscreen={() => {
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
PlatformInfo.setAudioActive(true)
player.muted = true
if (!player.playing) player.play()
}}
/>
<Controls
player={player}
enterFullscreen={() => ref.current?.enterFullscreen()}
/>
<Controls player={player} enterFullscreen={enterFullscreen} />
</View>
)
}
@@ -33,6 +82,9 @@ function Controls({
player: VideoPlayer
enterFullscreen: () => void
}) {
const {_} = useLingui()
const t = useTheme()
const [isMuted, setIsMuted] = useState(player.muted)
const [duration, setDuration] = useState(() => Math.floor(player.duration))
const [currentTime, setCurrentTime] = useState(() =>
Math.floor(player.currentTime),
@@ -47,50 +99,121 @@ function Controls({
// duration gets reset to 0 on loop
if (player.duration) setDuration(Math.floor(player.duration))
setCurrentTime(Math.floor(player.currentTime))
// how often should we update the time?
// 1000 gets out of sync with the video time
}, 250)
// eslint-disable-next-line @typescript-eslint/no-shadow
const sub = player.addListener('volumeChange', ({isMuted}) => {
setIsMuted(isMuted)
})
return () => {
clearInterval(interval)
sub.remove()
}
}, [player])
if (isNaN(timeRemaining)) {
return null
}
const onPressFullscreen = useCallback(() => {
switch (player.status) {
case 'idle':
case 'loading':
case 'readyToPlay': {
if (!player.playing) player.play()
enterFullscreen()
break
}
case 'error': {
player.replay()
break
}
}
}, [player, enterFullscreen])
const toggleMuted = useCallback(() => {
const muted = !player.muted
// We want to set this to the _inverse_ of the new value, because we actually want for the audio to be mixed when
// the video is muted, and vice versa.
const mix = !muted
const category = muted ? AudioCategory.Ambient : AudioCategory.Playback
PlatformInfo.setAudioCategory(category)
PlatformInfo.setAudioActive(mix)
player.muted = muted
}, [player])
// show countdown when:
// 1. timeRemaining is a number - was seeing NaNs
// 2. duration is greater than 0 - means metadata has loaded
// 3. we're less than 5 second into the video
const showTime = !isNaN(timeRemaining) && duration > 0 && currentTime <= 5
return (
<View style={[a.absolute, a.inset_0]}>
<View
style={[
{
backgroundColor: 'rgba(0, 0, 0, 0.75',
{showTime && (
<Animated.View
entering={FadeInDown.duration(300)}
exiting={FadeOutDown.duration(500)}
style={[
{
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
position: 'absolute',
left: 5,
bottom: 5,
minHeight: 20,
justifyContent: 'center',
},
]}>
<Text
style={[
{color: t.palette.white, fontSize: 12},
a.font_bold,
android({lineHeight: 1.25}),
]}>
{minutes}:{seconds}
</Text>
</Animated.View>
)}
<Pressable
onPress={onPressFullscreen}
style={a.flex_1}
accessibilityLabel={_(msg`Video`)}
accessibilityHint={_(msg`Tap to enter full screen`)}
accessibilityRole="button"
/>
{duration > 0 && (
<Animated.View
entering={FadeInDown.duration(300)}
style={{
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
position: 'absolute',
left: 5,
bottom: 5,
},
]}
pointerEvents="none">
<Text
style={[
{color: 'white', fontSize: 12},
a.font_bold,
android({lineHeight: 1.25}),
]}>
{minutes}:{seconds}
</Text>
</View>
<Pressable
onPress={enterFullscreen}
style={a.flex_1}
accessibilityLabel="Video"
accessibilityHint="Tap to enter full screen"
accessibilityRole="button"
/>
right: 5,
minHeight: 20,
justifyContent: 'center',
}}>
<Pressable
onPress={toggleMuted}
style={a.flex_1}
accessibilityLabel={isMuted ? _(msg`Muted`) : _(msg`Unmuted`)}
accessibilityHint={_(msg`Tap to toggle sound`)}
accessibilityRole="button"
hitSlop={HITSLOP_30}>
{isMuted ? (
<MuteIcon width={14} fill={t.palette.white} />
) : (
<UnmuteIcon width={14} fill={t.palette.white} />
)}
</Pressable>
</Animated.View>
)}
</View>
)
}
@@ -0,0 +1,61 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text as TypoText} from '#/components/Typography'
export function Container({children}: {children: React.ReactNode}) {
const t = useTheme()
return (
<View
style={[
a.flex_1,
t.atoms.bg_contrast_25,
a.justify_center,
a.align_center,
a.px_lg,
a.border,
t.atoms.border_contrast_low,
a.rounded_sm,
a.gap_lg,
]}>
{children}
</View>
)
}
export function Text({children}: {children: React.ReactNode}) {
const t = useTheme()
return (
<TypoText
style={[
a.text_center,
t.atoms.text_contrast_high,
a.text_md,
a.leading_snug,
{maxWidth: 300},
]}>
{children}
</TypoText>
)
}
export function RetryButton({onPress}: {onPress: () => void}) {
const {_} = useLingui()
return (
<Button
onPress={onPress}
size="small"
color="secondary_inverted"
variant="solid"
label={_(msg`Retry`)}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
)
}
@@ -14,6 +14,7 @@ export function VideoPlayerProvider({
// eslint-disable-next-line @typescript-eslint/no-shadow
const player = useExpoVideoPlayer(source, player => {
player.loop = true
player.muted = true
player.play()
})