Merge remote-tracking branch 'origin/main' into font-scaling
* origin/main: bump (#5348) Release 1.91.1 prep (#5339) [Video] Fix regression on audio session at launch (#5338) Don't open composer via hotkey if other dialog is already open (#5334) "N" keyboard shortcut to open a new post modal (#5197) [Video] Bump video (#5333) Apply Following settings to Lists (#5313) Always display next button on login page (#5326) [Video] Don't require email verification on self-host (#5332) [Video] Remember mute state while scrolling (#5331) Fix lexicon validation in PWI Discover (#5329) [Video] Remove `expo-video`, use `bluesky-video` (#5282)
This commit is contained in:
@@ -74,8 +74,7 @@ appId: xyz.blueskyweb.app
|
|||||||
- tapOn: "Delete List"
|
- tapOn: "Delete List"
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "confirmBtn"
|
id: "confirmBtn"
|
||||||
- assertVisible:
|
- assertVisible: "This list is empty!"
|
||||||
id: "listsEmpty"
|
|
||||||
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
label: "Create a new curatelist"
|
label: "Create a new curatelist"
|
||||||
@@ -161,17 +160,6 @@ appId: xyz.blueskyweb.app
|
|||||||
- assertNotVisible:
|
- assertNotVisible:
|
||||||
id: "userAddRemoveListsModal"
|
id: "userAddRemoveListsModal"
|
||||||
|
|
||||||
- tapOn:
|
|
||||||
label: "Shows the curatelist on my profile"
|
|
||||||
id: "bottomBarProfileBtn"
|
|
||||||
- swipe:
|
|
||||||
from:
|
|
||||||
id: "profilePager-selector"
|
|
||||||
direction: LEFT
|
|
||||||
- tapOn:
|
|
||||||
id: "profilePager-selector-6"
|
|
||||||
- tapOn: "Good Ppl"
|
|
||||||
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
label: "Adds and removes users on curatelists from the profile"
|
label: "Adds and removes users on curatelists from the profile"
|
||||||
id: "bottomBarSearchBtn"
|
id: "bottomBarSearchBtn"
|
||||||
|
|||||||
@@ -21,14 +21,12 @@ appId: xyz.blueskyweb.app
|
|||||||
id: "likeBtn"
|
id: "likeBtn"
|
||||||
childOf:
|
childOf:
|
||||||
id: "postThreadItem-by-bob.test"
|
id: "postThreadItem-by-bob.test"
|
||||||
- assertVisible:
|
- assertVisible: "1 like"
|
||||||
id: "likeCount-expanded"
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "likeBtn"
|
id: "likeBtn"
|
||||||
childOf:
|
childOf:
|
||||||
id: "postThreadItem-by-bob.test"
|
id: "postThreadItem-by-bob.test"
|
||||||
- assertNotVisible:
|
- assertNotVisible: "1 like"
|
||||||
id: "likeCount-expanded"
|
|
||||||
|
|
||||||
# Can like a reply post
|
# Can like a reply post
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|||||||
@@ -211,7 +211,6 @@ module.exports = function (config) {
|
|||||||
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'expo-video',
|
|
||||||
'react-native-compressor',
|
'react-native-compressor',
|
||||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||||
'./plugins/withAndroidManifestPlugin.js',
|
'./plugins/withAndroidManifestPlugin.js',
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bsky.app",
|
"name": "bsky.app",
|
||||||
"version": "1.91.0",
|
"version": "1.91.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||||
|
"@haileyok/bluesky-video": "0.1.8",
|
||||||
"@lingui/react": "^4.5.0",
|
"@lingui/react": "^4.5.0",
|
||||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||||
"@miblanchard/react-native-slider": "^2.3.1",
|
"@miblanchard/react-native-slider": "^2.3.1",
|
||||||
@@ -140,7 +141,6 @@
|
|||||||
"expo-system-ui": "~3.0.4",
|
"expo-system-ui": "~3.0.4",
|
||||||
"expo-task-manager": "~11.8.1",
|
"expo-task-manager": "~11.8.1",
|
||||||
"expo-updates": "~0.25.14",
|
"expo-updates": "~0.25.14",
|
||||||
"expo-video": "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz",
|
|
||||||
"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",
|
||||||
|
|||||||
+8
-10
@@ -52,10 +52,10 @@ 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 {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoNativeContext'
|
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||||
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, useFonts} from '#/alf'
|
import {ThemeProvider as Alf} from '#/alf'
|
||||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
@@ -63,7 +63,6 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
|
|||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army'
|
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
@@ -73,7 +72,6 @@ function InnerApp() {
|
|||||||
const {resumeSession} = useSessionApi()
|
const {resumeSession} = useSessionApi()
|
||||||
const theme = useColorModeTheme()
|
const theme = useColorModeTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const [fontsLoaded] = useFonts()
|
|
||||||
|
|
||||||
useIntentHandler()
|
useIntentHandler()
|
||||||
const hasCheckedReferrer = useStarterPackEntry()
|
const hasCheckedReferrer = useStarterPackEntry()
|
||||||
@@ -108,12 +106,14 @@ function InnerApp() {
|
|||||||
}, [_])
|
}, [_])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsigProvider key={currentAccount?.did}>
|
<StatsigProvider
|
||||||
|
// Resets the entire tree below when it changes:
|
||||||
|
key={currentAccount?.did}>
|
||||||
<Alf theme={theme}>
|
<Alf theme={theme}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Splash isReady={isReady && hasCheckedReferrer && fontsLoaded}>
|
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||||
<ActiveVideoProvider>
|
|
||||||
<RootSiblingParent>
|
<RootSiblingParent>
|
||||||
|
<VideoVolumeProvider>
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<MessagesProvider>
|
<MessagesProvider>
|
||||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||||
@@ -142,8 +142,8 @@ function InnerApp() {
|
|||||||
</LabelDefsProvider>
|
</LabelDefsProvider>
|
||||||
</MessagesProvider>
|
</MessagesProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
|
</VideoVolumeProvider>
|
||||||
</RootSiblingParent>
|
</RootSiblingParent>
|
||||||
</ActiveVideoProvider>
|
|
||||||
</Splash>
|
</Splash>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Alf>
|
</Alf>
|
||||||
@@ -155,8 +155,6 @@ function App() {
|
|||||||
const [isReady, setReady] = useState(false)
|
const [isReady, setReady] = useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
|
||||||
PlatformInfo.setAudioActive(false)
|
|
||||||
initPersistedState().then(() => setReady(true))
|
initPersistedState().then(() => setReady(true))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -35,16 +35,18 @@ import {
|
|||||||
} from '#/state/session'
|
} from '#/state/session'
|
||||||
import {readLastActiveAccount} from '#/state/session/util'
|
import {readLastActiveAccount} from '#/state/session/util'
|
||||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||||
|
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
|
||||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
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 {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
||||||
|
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||||
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'
|
||||||
import {ThemeProvider as Alf, useFonts} from '#/alf'
|
import {ThemeProvider as Alf} from '#/alf'
|
||||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
@@ -60,7 +62,8 @@ function InnerApp() {
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
useIntentHandler()
|
useIntentHandler()
|
||||||
const hasCheckedReferrer = useStarterPackEntry()
|
const hasCheckedReferrer = useStarterPackEntry()
|
||||||
const [fontsLoaded] = useFonts()
|
|
||||||
|
useComposerKeyboardShortcut()
|
||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -89,14 +92,17 @@ function InnerApp() {
|
|||||||
}, [_])
|
}, [_])
|
||||||
|
|
||||||
// wait for session to resume
|
// wait for session to resume
|
||||||
if (!isReady || !hasCheckedReferrer || !fontsLoaded) return null
|
if (!isReady || !hasCheckedReferrer) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardProvider enabled={false}>
|
<KeyboardProvider enabled={false}>
|
||||||
<StatsigProvider key={currentAccount?.did}>
|
<StatsigProvider
|
||||||
|
// Resets the entire tree below when it changes:
|
||||||
|
key={currentAccount?.did}>
|
||||||
<Alf theme={theme}>
|
<Alf theme={theme}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<RootSiblingParent>
|
<RootSiblingParent>
|
||||||
|
<VideoVolumeProvider>
|
||||||
<ActiveVideoProvider>
|
<ActiveVideoProvider>
|
||||||
<QueryProvider currentDid={currentAccount?.did}>
|
<QueryProvider currentDid={currentAccount?.did}>
|
||||||
<MessagesProvider>
|
<MessagesProvider>
|
||||||
@@ -127,6 +133,7 @@ function InnerApp() {
|
|||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</ActiveVideoProvider>
|
</ActiveVideoProvider>
|
||||||
|
</VideoVolumeProvider>
|
||||||
</RootSiblingParent>
|
</RootSiblingParent>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Alf>
|
</Alf>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface ProfilesListProps {
|
|||||||
|
|
||||||
export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
|
export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||||
function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) {
|
function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) {
|
||||||
const feed: FeedDescriptor = `list|${listUri}|as_following`
|
const feed: FeedDescriptor = `list|${listUri}`
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const onScrollToTop = useCallback(() => {
|
const onScrollToTop = useCallback(() => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {View} from 'react-native'
|
|||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||||
|
|
||||||
export function PlayButtonIcon({size = 36}: {size?: number}) {
|
export function PlayButtonIcon({size = 32}: {size?: number}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975
|
const bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975
|
||||||
const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25
|
const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedGetFeed as GetCustomFeed,
|
AppBskyFeedGetFeed as GetCustomFeed,
|
||||||
BskyAgent,
|
BskyAgent,
|
||||||
|
jsonStringToLex,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {getContentLanguages} from '#/state/preferences/languages'
|
import {getContentLanguages} from '#/state/preferences/languages'
|
||||||
@@ -111,7 +112,7 @@ async function loggedOutFetch({
|
|||||||
}&limit=${limit}&lang=${contentLangs}`,
|
}&limit=${limit}&lang=${contentLangs}`,
|
||||||
{method: 'GET', headers: {'Accept-Language': contentLangs}},
|
{method: 'GET', headers: {'Accept-Language': contentLangs}},
|
||||||
)
|
)
|
||||||
let data = res.ok ? await res.json() : null
|
let data = res.ok ? jsonStringToLex(await res.text()) : null
|
||||||
if (data?.feed?.length) {
|
if (data?.feed?.length) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -126,7 +127,7 @@ async function loggedOutFetch({
|
|||||||
}&limit=${limit}`,
|
}&limit=${limit}`,
|
||||||
{method: 'GET', headers: {'Accept-Language': ''}},
|
{method: 'GET', headers: {'Accept-Language': ''}},
|
||||||
)
|
)
|
||||||
data = res.ok ? await res.json() : null
|
data = res.ok ? jsonStringToLex(await res.text()) : null
|
||||||
if (data?.feed?.length) {
|
if (data?.feed?.length) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
+265
-182
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
+219
-153
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
+190
-169
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+388
-238
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
+265
-182
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -144,8 +144,7 @@ export const ForgotPasswordForm = ({
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
color={'primary'}
|
color={'primary'}
|
||||||
size="medium"
|
size="medium"
|
||||||
onPress={onPressNext}
|
onPress={onPressNext}>
|
||||||
disabled={!email}>
|
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Next</Trans>
|
<Trans>Next</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ export const LoginForm = ({
|
|||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||||
const [isReady, setIsReady] = useState<boolean>(false)
|
|
||||||
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
|
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
|
||||||
useState<boolean>(false)
|
useState<boolean>(false)
|
||||||
const identifierValueRef = useRef<string>(initialHandle || '')
|
const identifierValueRef = useRef<string>(initialHandle || '')
|
||||||
@@ -83,12 +82,18 @@ export const LoginForm = ({
|
|||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||||
setError('')
|
setError('')
|
||||||
setIsProcessing(true)
|
|
||||||
|
|
||||||
const identifier = identifierValueRef.current.toLowerCase().trim()
|
const identifier = identifierValueRef.current.toLowerCase().trim()
|
||||||
const password = passwordValueRef.current
|
const password = passwordValueRef.current
|
||||||
const authFactorToken = authFactorTokenValueRef.current
|
const authFactorToken = authFactorTokenValueRef.current
|
||||||
|
|
||||||
|
if (!identifier || !password) {
|
||||||
|
setError(_(msg`Invalid username or password`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// try to guess the handle if the user just gave their own username
|
// try to guess the handle if the user just gave their own username
|
||||||
let fullIdent = identifier
|
let fullIdent = identifier
|
||||||
@@ -157,22 +162,6 @@ export const LoginForm = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkIsReady = () => {
|
|
||||||
if (
|
|
||||||
!!serviceDescription &&
|
|
||||||
!!identifierValueRef.current &&
|
|
||||||
!!passwordValueRef.current
|
|
||||||
) {
|
|
||||||
if (!isReady) {
|
|
||||||
setIsReady(true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isReady) {
|
|
||||||
setIsReady(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
|
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
|
||||||
<View>
|
<View>
|
||||||
@@ -204,7 +193,6 @@ export const LoginForm = ({
|
|||||||
defaultValue={initialHandle || ''}
|
defaultValue={initialHandle || ''}
|
||||||
onChangeText={v => {
|
onChangeText={v => {
|
||||||
identifierValueRef.current = v
|
identifierValueRef.current = v
|
||||||
checkIsReady()
|
|
||||||
}}
|
}}
|
||||||
onSubmitEditing={() => {
|
onSubmitEditing={() => {
|
||||||
passwordRef.current?.focus()
|
passwordRef.current?.focus()
|
||||||
@@ -233,7 +221,6 @@ export const LoginForm = ({
|
|||||||
clearButtonMode="while-editing"
|
clearButtonMode="while-editing"
|
||||||
onChangeText={v => {
|
onChangeText={v => {
|
||||||
passwordValueRef.current = v
|
passwordValueRef.current = v
|
||||||
checkIsReady()
|
|
||||||
}}
|
}}
|
||||||
onSubmitEditing={onPressNext}
|
onSubmitEditing={onPressNext}
|
||||||
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
|
||||||
@@ -325,7 +312,7 @@ export const LoginForm = ({
|
|||||||
<Trans>Connecting...</Trans>
|
<Trans>Connecting...</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : isReady ? (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
testID="loginNextButton"
|
testID="loginNextButton"
|
||||||
label={_(msg`Next`)}
|
label={_(msg`Next`)}
|
||||||
@@ -339,7 +326,7 @@ export const LoginForm = ({
|
|||||||
</ButtonText>
|
</ButtonText>
|
||||||
{isProcessing && <ButtonIcon icon={Loader} />}
|
{isProcessing && <ButtonIcon icon={Loader} />}
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,31 +21,7 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
|
|||||||
if (feedDesc.startsWith('feedgen')) {
|
if (feedDesc.startsWith('feedgen')) {
|
||||||
return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)]
|
return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)]
|
||||||
}
|
}
|
||||||
if (feedDesc.startsWith('list')) {
|
if (feedDesc === 'following' || feedDesc.startsWith('list')) {
|
||||||
let feedTuners = []
|
|
||||||
if (feedDesc.endsWith('|as_following')) {
|
|
||||||
// Same as Following tuners below, copypaste for now.
|
|
||||||
feedTuners.push(FeedTuner.removeOrphans)
|
|
||||||
if (preferences?.feedViewPrefs.hideReposts) {
|
|
||||||
feedTuners.push(FeedTuner.removeReposts)
|
|
||||||
}
|
|
||||||
if (preferences?.feedViewPrefs.hideReplies) {
|
|
||||||
feedTuners.push(FeedTuner.removeReplies)
|
|
||||||
} else {
|
|
||||||
feedTuners.push(
|
|
||||||
FeedTuner.followedRepliesOnly({
|
|
||||||
userDid: currentAccount?.did || '',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (preferences?.feedViewPrefs.hideQuotePosts) {
|
|
||||||
feedTuners.push(FeedTuner.removeQuotePosts)
|
|
||||||
}
|
|
||||||
feedTuners.push(FeedTuner.dedupThreads)
|
|
||||||
}
|
|
||||||
return feedTuners
|
|
||||||
}
|
|
||||||
if (feedDesc === 'following') {
|
|
||||||
const feedTuners = [FeedTuner.removeOrphans]
|
const feedTuners = [FeedTuner.removeOrphans]
|
||||||
|
|
||||||
if (preferences?.feedViewPrefs.hideReposts) {
|
if (preferences?.feedViewPrefs.hideReposts) {
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ type AuthorFilter =
|
|||||||
| 'posts_with_media'
|
| 'posts_with_media'
|
||||||
type FeedUri = string
|
type FeedUri = string
|
||||||
type ListUri = string
|
type ListUri = string
|
||||||
type ListFilter = 'as_following' // Applies current Following settings. Currently client-side.
|
|
||||||
|
|
||||||
export type FeedDescriptor =
|
export type FeedDescriptor =
|
||||||
| 'following'
|
| 'following'
|
||||||
@@ -59,7 +58,6 @@ export type FeedDescriptor =
|
|||||||
| `feedgen|${FeedUri}`
|
| `feedgen|${FeedUri}`
|
||||||
| `likes|${ActorDid}`
|
| `likes|${ActorDid}`
|
||||||
| `list|${ListUri}`
|
| `list|${ListUri}`
|
||||||
| `list|${ListUri}|${ListFilter}`
|
|
||||||
export interface FeedParams {
|
export interface FeedParams {
|
||||||
mergeFeedEnabled?: boolean
|
mergeFeedEnabled?: boolean
|
||||||
mergeFeedSources?: string[]
|
mergeFeedSources?: string[]
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {useDialogStateContext} from '#/state/dialogs'
|
||||||
|
import {useLightbox} from '#/state/lightbox'
|
||||||
|
import {useModals} from '#/state/modals'
|
||||||
|
import {useIsDrawerOpen} from '#/state/shell/drawer-open'
|
||||||
|
import {useComposerControls} from './'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on {@link https://github.com/jaywcjlove/hotkeys-js/blob/b0038773f3b902574f22af747f3bb003a850f1da/src/index.js#L51C1-L64C2}
|
||||||
|
*/
|
||||||
|
function shouldIgnore(event: KeyboardEvent) {
|
||||||
|
const target: any = event.target || event.srcElement
|
||||||
|
if (!target) return false
|
||||||
|
const {tagName} = target
|
||||||
|
if (!tagName) return false
|
||||||
|
const isInput =
|
||||||
|
tagName === 'INPUT' &&
|
||||||
|
![
|
||||||
|
'checkbox',
|
||||||
|
'radio',
|
||||||
|
'range',
|
||||||
|
'button',
|
||||||
|
'file',
|
||||||
|
'reset',
|
||||||
|
'submit',
|
||||||
|
'color',
|
||||||
|
].includes(target.type)
|
||||||
|
// ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
|
||||||
|
if (
|
||||||
|
target.isContentEditable ||
|
||||||
|
((isInput || tagName === 'TEXTAREA' || tagName === 'SELECT') &&
|
||||||
|
!target.readOnly)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useComposerKeyboardShortcut() {
|
||||||
|
const {openComposer} = useComposerControls()
|
||||||
|
const {openDialogs} = useDialogStateContext()
|
||||||
|
const {isModalActive} = useModals()
|
||||||
|
const {activeLightbox} = useLightbox()
|
||||||
|
const isDrawerOpen = useIsDrawerOpen()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function handler(event: KeyboardEvent) {
|
||||||
|
if (shouldIgnore(event)) return
|
||||||
|
if (
|
||||||
|
openDialogs.current.size > 0 ||
|
||||||
|
isModalActive ||
|
||||||
|
activeLightbox ||
|
||||||
|
isDrawerOpen
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if (event.key === 'n' || event.key === 'N') {
|
||||||
|
openComposer({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', handler)
|
||||||
|
return () => document.removeEventListener('keydown', handler)
|
||||||
|
}, [openComposer, isModalActive, openDialogs, activeLightbox, isDrawerOpen])
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import {useVideoLibraryPermission} from '#/lib/hooks/usePermissions'
|
|||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {BSKY_SERVICE} from 'lib/constants'
|
||||||
|
import {getHostnameFromUrl} from 'lib/strings/url-helpers'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip'
|
import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip'
|
||||||
@@ -38,7 +40,12 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentAccount?.emailConfirmed) {
|
if (
|
||||||
|
currentAccount &&
|
||||||
|
!currentAccount.emailConfirmed &&
|
||||||
|
getHostnameFromUrl(currentAccount.service) ===
|
||||||
|
getHostnameFromUrl(BSKY_SERVICE)
|
||||||
|
) {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
control.open()
|
control.open()
|
||||||
} else {
|
} else {
|
||||||
@@ -71,12 +78,12 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
onSelectVideo,
|
|
||||||
requestVideoAccessIfNeeded,
|
requestVideoAccessIfNeeded,
|
||||||
|
currentAccount,
|
||||||
|
control,
|
||||||
setError,
|
setError,
|
||||||
_,
|
_,
|
||||||
control,
|
onSelectVideo,
|
||||||
currentAccount?.emailConfirmed,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-shadow */
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {ImagePickerAsset} from 'expo-image-picker'
|
import {ImagePickerAsset} from 'expo-image-picker'
|
||||||
import {useVideoPlayer, VideoView} from 'expo-video'
|
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||||
|
|
||||||
import {CompressedVideo} from '#/lib/media/video/types'
|
import {CompressedVideo} from '#/lib/media/video/types'
|
||||||
import {clamp} from '#/lib/numbers'
|
import {clamp} from '#/lib/numbers'
|
||||||
@@ -22,15 +21,8 @@ export function VideoPreview({
|
|||||||
clear: () => void
|
clear: () => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const playerRef = React.useRef<BlueskyVideoView>(null)
|
||||||
const autoplayDisabled = useAutoplayDisabled()
|
const autoplayDisabled = useAutoplayDisabled()
|
||||||
const player = useVideoPlayer(video.uri, player => {
|
|
||||||
player.loop = true
|
|
||||||
player.muted = true
|
|
||||||
if (!autoplayDisabled) {
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let aspectRatio = asset.width / asset.height
|
let aspectRatio = asset.width / asset.height
|
||||||
|
|
||||||
if (isNaN(aspectRatio)) {
|
if (isNaN(aspectRatio)) {
|
||||||
@@ -50,12 +42,12 @@ export function VideoPreview({
|
|||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
{backgroundColor: 'black'},
|
{backgroundColor: 'black'},
|
||||||
]}>
|
]}>
|
||||||
<VideoView
|
<BlueskyVideoView
|
||||||
player={player}
|
url={video.uri}
|
||||||
style={a.flex_1}
|
autoplay={autoplayDisabled}
|
||||||
allowsPictureInPicture={false}
|
beginMuted={true}
|
||||||
nativeControls={false}
|
forceTakeover={true}
|
||||||
contentFit="contain"
|
ref={playerRef}
|
||||||
/>
|
/>
|
||||||
<ExternalEmbedRemoveBtn onRemove={clear} />
|
<ExternalEmbedRemoveBtn onRemove={clear} />
|
||||||
{autoplayDisabled && (
|
{autoplayDisabled && (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, {memo} from 'react'
|
import React, {memo} from 'react'
|
||||||
import {FlatListProps, RefreshControl, ViewToken} from 'react-native'
|
import {FlatListProps, RefreshControl, ViewToken} from 'react-native'
|
||||||
import {runOnJS, useSharedValue} from 'react-native-reanimated'
|
import {runOnJS, useSharedValue} from 'react-native-reanimated'
|
||||||
|
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
|
||||||
|
|
||||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
@@ -8,7 +9,6 @@ import {useScrollHandlers} from '#/lib/ScrollContext'
|
|||||||
import {useDedupe} from 'lib/hooks/useDedupe'
|
import {useDedupe} from 'lib/hooks/useDedupe'
|
||||||
import {addStyle} from 'lib/styles'
|
import {addStyle} from 'lib/styles'
|
||||||
import {isIOS} from 'platform/detection'
|
import {isIOS} from 'platform/detection'
|
||||||
import {updateActiveViewAsync} from '../../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
|
|
||||||
import {FlatList_INTERNAL} from './Views'
|
import {FlatList_INTERNAL} from './Views'
|
||||||
|
|
||||||
export type ListMethods = FlatList_INTERNAL
|
export type ListMethods = FlatList_INTERNAL
|
||||||
@@ -69,7 +69,7 @@ function ListImpl<ItemT>(
|
|||||||
onBeginDragFromContext?.(e, ctx)
|
onBeginDragFromContext?.(e, ctx)
|
||||||
},
|
},
|
||||||
onEndDrag(e, ctx) {
|
onEndDrag(e, ctx) {
|
||||||
runOnJS(updateActiveViewAsync)()
|
runOnJS(updateActiveVideoViewAsync)()
|
||||||
onEndDragFromContext?.(e, ctx)
|
onEndDragFromContext?.(e, ctx)
|
||||||
},
|
},
|
||||||
onScroll(e, ctx) {
|
onScroll(e, ctx) {
|
||||||
@@ -84,13 +84,13 @@ function ListImpl<ItemT>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
runOnJS(dedupe)(updateActiveViewAsync)
|
runOnJS(dedupe)(updateActiveVideoViewAsync)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Note: adding onMomentumBegin here makes simulator scroll
|
// Note: adding onMomentumBegin here makes simulator scroll
|
||||||
// lag on Android. So either don't add it, or figure out why.
|
// lag on Android. So either don't add it, or figure out why.
|
||||||
onMomentumEnd(e, ctx) {
|
onMomentumEnd(e, ctx) {
|
||||||
runOnJS(updateActiveViewAsync)()
|
runOnJS(updateActiveVideoViewAsync)()
|
||||||
onMomentumEndFromContext?.(e, ctx)
|
onMomentumEndFromContext?.(e, ctx)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {useVideoPlayer, VideoPlayer} from 'expo-video'
|
|
||||||
|
|
||||||
import {isAndroid, isNative} from '#/platform/detection'
|
|
||||||
|
|
||||||
const Context = React.createContext<{
|
|
||||||
activeSource: string
|
|
||||||
activeViewId: string | undefined
|
|
||||||
setActiveSource: (src: string | null, viewId: string | null) => void
|
|
||||||
player: VideoPlayer
|
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
export function Provider({children}: {children: React.ReactNode}) {
|
|
||||||
if (!isNative) {
|
|
||||||
throw new Error('ActiveVideoProvider may only be used on native.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const [activeSource, setActiveSource] = React.useState('')
|
|
||||||
const [activeViewId, setActiveViewId] = React.useState<string>()
|
|
||||||
|
|
||||||
const player = useVideoPlayer(activeSource, p => {
|
|
||||||
p.muted = true
|
|
||||||
p.loop = true
|
|
||||||
// We want to immediately call `play` so we get the loading state
|
|
||||||
p.play()
|
|
||||||
})
|
|
||||||
|
|
||||||
const setActiveSourceOuter = (src: string | null, viewId: string | null) => {
|
|
||||||
// HACK
|
|
||||||
// expo-video doesn't like it when you try and move a `player` to another `VideoView`. Instead, we need to actually
|
|
||||||
// unregister that player to let the new screen register it. This is only a problem on Android, so we only need to
|
|
||||||
// apply it there.
|
|
||||||
if (src === activeSource && isAndroid) {
|
|
||||||
setActiveSource('')
|
|
||||||
setTimeout(() => {
|
|
||||||
setActiveSource(src ? src : '')
|
|
||||||
}, 100)
|
|
||||||
} else {
|
|
||||||
setActiveSource(src ? src : '')
|
|
||||||
}
|
|
||||||
setActiveViewId(viewId ? viewId : '')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Context.Provider
|
|
||||||
value={{
|
|
||||||
activeSource,
|
|
||||||
setActiveSource: setActiveSourceOuter,
|
|
||||||
activeViewId,
|
|
||||||
player,
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</Context.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useActiveVideoNative() {
|
|
||||||
const context = React.useContext(Context)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
'useActiveVideoNative must be used within a ActiveVideoNativeProvider',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,18 @@
|
|||||||
import React, {useCallback, useEffect, useId, useState} from 'react'
|
import React, {useCallback, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {ImageBackground} from 'expo-image'
|
import {ImageBackground} from 'expo-image'
|
||||||
import {PlayerError, VideoPlayerStatus} from 'expo-video'
|
|
||||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {clamp} from '#/lib/numbers'
|
import {clamp} from '#/lib/numbers'
|
||||||
import {useAutoplayDisabled} from 'state/preferences'
|
|
||||||
import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||||
import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
|
|
||||||
import {ErrorBoundary} from '../ErrorBoundary'
|
import {ErrorBoundary} from '../ErrorBoundary'
|
||||||
import {useActiveVideoNative} from './ActiveVideoNativeContext'
|
|
||||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -59,113 +55,36 @@ export function VideoEmbed({embed}: Props) {
|
|||||||
|
|
||||||
function InnerWrapper({embed}: Props) {
|
function InnerWrapper({embed}: Props) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {activeSource, activeViewId, setActiveSource, player} =
|
const ref = React.useRef<{togglePlayback: () => void}>(null)
|
||||||
useActiveVideoNative()
|
|
||||||
const viewId = useId()
|
|
||||||
|
|
||||||
const [playerStatus, setPlayerStatus] = useState<
|
const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>(
|
||||||
VideoPlayerStatus | 'paused'
|
'pending',
|
||||||
>('paused')
|
|
||||||
const [isMuted, setIsMuted] = useState(player.muted)
|
|
||||||
const [isFullscreen, setIsFullscreen] = React.useState(false)
|
|
||||||
const [timeRemaining, setTimeRemaining] = React.useState(0)
|
|
||||||
const isWithinMessage = useIsWithinMessage()
|
|
||||||
const disableAutoplay = useAutoplayDisabled() || isWithinMessage
|
|
||||||
const isActive = embed.playlist === activeSource && activeViewId === viewId
|
|
||||||
// There are some different loading states that we should pay attention to and show a spinner for
|
|
||||||
const isLoading =
|
|
||||||
isActive &&
|
|
||||||
(playerStatus === 'waitingToPlayAtSpecifiedRate' ||
|
|
||||||
playerStatus === 'loading')
|
|
||||||
// This happens whenever the visibility view decides that another video should start playing
|
|
||||||
const showOverlay = !isActive || isLoading || playerStatus === 'paused'
|
|
||||||
|
|
||||||
// send error up to error boundary
|
|
||||||
const [error, setError] = useState<Error | PlayerError | null>(null)
|
|
||||||
if (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isActive) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
||||||
const volumeSub = player.addListener('volumeChange', ({isMuted}) => {
|
|
||||||
setIsMuted(isMuted)
|
|
||||||
})
|
|
||||||
const timeSub = player.addListener(
|
|
||||||
'timeRemainingChange',
|
|
||||||
secondsRemaining => {
|
|
||||||
setTimeRemaining(secondsRemaining)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
const statusSub = player.addListener(
|
const [isLoading, setIsLoading] = React.useState(false)
|
||||||
'statusChange',
|
const [isActive, setIsActive] = React.useState(false)
|
||||||
(status, oldStatus, playerError) => {
|
const showSpinner = useThrottledValue(isActive && isLoading, 100)
|
||||||
setPlayerStatus(status)
|
|
||||||
if (status === 'error') {
|
|
||||||
setError(playerError ?? new Error('Unknown player error'))
|
|
||||||
}
|
|
||||||
if (status === 'readyToPlay' && oldStatus !== 'readyToPlay') {
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return () => {
|
|
||||||
volumeSub.remove()
|
|
||||||
timeSub.remove()
|
|
||||||
statusSub.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [player, isActive, disableAutoplay])
|
|
||||||
|
|
||||||
// The source might already be active (for example, if you are scrolling a list of quotes and its all the same
|
const showOverlay =
|
||||||
// video). In those cases, just start playing. Otherwise, setting the active source will result in the video
|
!isActive ||
|
||||||
// start playback immediately
|
isLoading ||
|
||||||
const startPlaying = (ignoreAutoplayPreference: boolean) => {
|
(status === 'paused' && !isActive) ||
|
||||||
if (disableAutoplay && !ignoreAutoplayPreference) {
|
status === 'pending'
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActive) {
|
React.useEffect(() => {
|
||||||
player.play()
|
if (!isActive && status !== 'pending') {
|
||||||
} else {
|
setStatus('pending')
|
||||||
setActiveSource(embed.playlist, viewId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onVisibilityStatusChange = (isVisible: boolean) => {
|
|
||||||
// When `isFullscreen` is true, it means we're actually still exiting the fullscreen player. Ignore these change
|
|
||||||
// events
|
|
||||||
if (isFullscreen) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isVisible) {
|
|
||||||
startPlaying(false)
|
|
||||||
} else {
|
|
||||||
// Clear the active source so the video view unmounts when autoplay is disabled. Otherwise, leave it mounted
|
|
||||||
// until it gets replaced by another video
|
|
||||||
if (disableAutoplay) {
|
|
||||||
setActiveSource(null, null)
|
|
||||||
} else {
|
|
||||||
player.muted = true
|
|
||||||
if (player.playing) {
|
|
||||||
player.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [isActive, status])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VisibilityView enabled={true} onChangeStatus={onVisibilityStatusChange}>
|
<>
|
||||||
{isActive ? (
|
|
||||||
<VideoEmbedInnerNative
|
<VideoEmbedInnerNative
|
||||||
embed={embed}
|
embed={embed}
|
||||||
timeRemaining={timeRemaining}
|
setStatus={setStatus}
|
||||||
isMuted={isMuted}
|
setIsLoading={setIsLoading}
|
||||||
isFullscreen={isFullscreen}
|
setIsActive={setIsActive}
|
||||||
setIsFullscreen={setIsFullscreen}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
<ImageBackground
|
<ImageBackground
|
||||||
source={{uri: embed.thumbnail}}
|
source={{uri: embed.thumbnail}}
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
@@ -185,17 +104,18 @@ function InnerWrapper({embed}: Props) {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
style={[a.flex_1, a.align_center, a.justify_center]}
|
style={[a.flex_1, a.align_center, a.justify_center]}
|
||||||
onPress={() => startPlaying(true)}
|
onPress={() => {
|
||||||
|
ref.current?.togglePlayback()
|
||||||
|
}}
|
||||||
label={_(msg`Play video`)}
|
label={_(msg`Play video`)}
|
||||||
color="secondary">
|
color="secondary">
|
||||||
{isLoading ? (
|
{showSpinner ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.rounded_full,
|
a.rounded_full,
|
||||||
a.p_xs,
|
a.p_xs,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
{backgroundColor: 'rgba(0,0,0,0.5)'},
|
|
||||||
]}>
|
]}>
|
||||||
<Loader size="2xl" style={{color: 'white'}} />
|
<Loader size="2xl" style={{color: 'white'}} />
|
||||||
</View>
|
</View>
|
||||||
@@ -204,7 +124,7 @@ function InnerWrapper({embed}: Props) {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</ImageBackground>
|
</ImageBackground>
|
||||||
</VisibilityView>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {StyleProp, ViewStyle} from 'react-native'
|
||||||
import Animated, {FadeInDown, FadeOutDown} from 'react-native-reanimated'
|
import Animated, {FadeInDown, FadeOutDown} from 'react-native-reanimated'
|
||||||
|
|
||||||
import {atoms as a, native, useTheme} from '#/alf'
|
import {atoms as a, native, useTheme} from '#/alf'
|
||||||
@@ -8,7 +9,13 @@ import {Text} from '#/components/Typography'
|
|||||||
* Absolutely positioned time indicator showing how many seconds are remaining
|
* Absolutely positioned time indicator showing how many seconds are remaining
|
||||||
* Time is in seconds
|
* Time is in seconds
|
||||||
*/
|
*/
|
||||||
export function TimeIndicator({time}: {time: number}) {
|
export function TimeIndicator({
|
||||||
|
time,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
time: number
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
if (isNaN(time)) {
|
if (isNaN(time)) {
|
||||||
@@ -22,18 +29,20 @@ export function TimeIndicator({time}: {time: number}) {
|
|||||||
<Animated.View
|
<Animated.View
|
||||||
entering={native(FadeInDown.duration(300))}
|
entering={native(FadeInDown.duration(300))}
|
||||||
exiting={native(FadeOutDown.duration(500))}
|
exiting={native(FadeOutDown.duration(500))}
|
||||||
|
pointerEvents="none"
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
paddingVertical: 3,
|
paddingVertical: 3,
|
||||||
position: 'absolute',
|
|
||||||
left: 6,
|
left: 6,
|
||||||
bottom: 6,
|
bottom: 6,
|
||||||
minHeight: 21,
|
minHeight: 21,
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
},
|
||||||
|
a.absolute,
|
||||||
|
a.justify_center,
|
||||||
|
style,
|
||||||
]}>
|
]}>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -1,45 +1,58 @@
|
|||||||
import React, {useCallback, useRef} from 'react'
|
import React, {useRef} from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
||||||
import Animated, {FadeInDown} from 'react-native-reanimated'
|
import Animated, {FadeInDown} from 'react-native-reanimated'
|
||||||
import {VideoPlayer, VideoView} from 'expo-video'
|
|
||||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||||
|
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {HITSLOP_30} from '#/lib/constants'
|
import {HITSLOP_30} from '#/lib/constants'
|
||||||
import {clamp} from '#/lib/numbers'
|
import {clamp} from '#/lib/numbers'
|
||||||
import {isAndroid} from 'platform/detection'
|
import {useAutoplayDisabled} from '#/state/preferences'
|
||||||
import {useActiveVideoNative} from 'view/com/util/post-embeds/ActiveVideoNativeContext'
|
import {useVideoVolumeState} from 'view/com/util/post-embeds/VideoVolumeContext'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||||
|
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
|
||||||
|
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import {
|
|
||||||
AudioCategory,
|
|
||||||
PlatformInfo,
|
|
||||||
} from '../../../../../../modules/expo-bluesky-swiss-army'
|
|
||||||
import {TimeIndicator} from './TimeIndicator'
|
import {TimeIndicator} from './TimeIndicator'
|
||||||
|
|
||||||
export function VideoEmbedInnerNative({
|
export const VideoEmbedInnerNative = React.forwardRef(
|
||||||
|
function VideoEmbedInnerNative(
|
||||||
|
{
|
||||||
embed,
|
embed,
|
||||||
isFullscreen,
|
setStatus,
|
||||||
setIsFullscreen,
|
setIsLoading,
|
||||||
isMuted,
|
setIsActive,
|
||||||
timeRemaining,
|
|
||||||
}: {
|
}: {
|
||||||
embed: AppBskyEmbedVideo.View
|
embed: AppBskyEmbedVideo.View
|
||||||
isFullscreen: boolean
|
setStatus: (status: 'playing' | 'paused') => void
|
||||||
setIsFullscreen: (isFullscreen: boolean) => void
|
setIsLoading: (isLoading: boolean) => void
|
||||||
timeRemaining: number
|
setIsActive: (isActive: boolean) => void
|
||||||
isMuted: boolean
|
},
|
||||||
}) {
|
ref: React.Ref<{togglePlayback: () => void}>,
|
||||||
|
) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {player} = useActiveVideoNative()
|
const videoRef = useRef<BlueskyVideoView>(null)
|
||||||
const ref = useRef<VideoView>(null)
|
const autoplayDisabled = useAutoplayDisabled()
|
||||||
|
const isWithinMessage = useIsWithinMessage()
|
||||||
|
const {muted, setMuted} = useVideoVolumeState()
|
||||||
|
|
||||||
const enterFullscreen = useCallback(() => {
|
const [isPlaying, setIsPlaying] = React.useState(false)
|
||||||
ref.current?.enterFullscreen()
|
const [timeRemaining, setTimeRemaining] = React.useState(0)
|
||||||
}, [])
|
const [error, setError] = React.useState<string>()
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
togglePlayback: () => {
|
||||||
|
videoRef.current?.togglePlayback()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
|
||||||
let aspectRatio = 16 / 9
|
let aspectRatio = 16 / 9
|
||||||
|
|
||||||
@@ -51,86 +64,71 @@ export function VideoEmbedInnerNative({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1, a.relative, {aspectRatio}]}>
|
<View style={[a.flex_1, a.relative, {aspectRatio}]}>
|
||||||
<VideoView
|
<BlueskyVideoView
|
||||||
ref={ref}
|
url={embed.playlist}
|
||||||
player={player}
|
autoplay={!autoplayDisabled && !isWithinMessage}
|
||||||
style={[a.flex_1, a.rounded_sm]}
|
beginMuted={autoplayDisabled ? false : muted}
|
||||||
contentFit="cover"
|
style={[a.rounded_sm]}
|
||||||
nativeControls={isFullscreen}
|
onActiveChange={e => {
|
||||||
accessibilityIgnoresInvertColors
|
setIsActive(e.nativeEvent.isActive)
|
||||||
onFullscreenEnter={() => {
|
|
||||||
PlatformInfo.setAudioCategory(AudioCategory.Playback)
|
|
||||||
PlatformInfo.setAudioActive(true)
|
|
||||||
player.muted = false
|
|
||||||
setIsFullscreen(true)
|
|
||||||
if (isAndroid) {
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onFullscreenExit={() => {
|
onLoadingChange={e => {
|
||||||
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
setIsLoading(e.nativeEvent.isLoading)
|
||||||
PlatformInfo.setAudioActive(false)
|
|
||||||
player.muted = true
|
|
||||||
player.playbackRate = 1
|
|
||||||
setIsFullscreen(false)
|
|
||||||
}}
|
}}
|
||||||
|
onMutedChange={e => {
|
||||||
|
setMuted(e.nativeEvent.isMuted)
|
||||||
|
}}
|
||||||
|
onStatusChange={e => {
|
||||||
|
setStatus(e.nativeEvent.status)
|
||||||
|
setIsPlaying(e.nativeEvent.status === 'playing')
|
||||||
|
}}
|
||||||
|
onTimeRemainingChange={e => {
|
||||||
|
setTimeRemaining(e.nativeEvent.timeRemaining)
|
||||||
|
}}
|
||||||
|
onError={e => {
|
||||||
|
setError(e.nativeEvent.error)
|
||||||
|
}}
|
||||||
|
ref={videoRef}
|
||||||
accessibilityLabel={
|
accessibilityLabel={
|
||||||
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
|
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
|
||||||
}
|
}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
<VideoControls
|
<VideoControls
|
||||||
player={player}
|
enterFullscreen={() => {
|
||||||
enterFullscreen={enterFullscreen}
|
videoRef.current?.enterFullscreen()
|
||||||
isMuted={isMuted}
|
}}
|
||||||
|
toggleMuted={() => {
|
||||||
|
videoRef.current?.toggleMuted()
|
||||||
|
}}
|
||||||
|
togglePlayback={() => {
|
||||||
|
videoRef.current?.togglePlayback()
|
||||||
|
}}
|
||||||
|
isPlaying={isPlaying}
|
||||||
timeRemaining={timeRemaining}
|
timeRemaining={timeRemaining}
|
||||||
/>
|
/>
|
||||||
<MediaInsetBorder />
|
<MediaInsetBorder />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
function VideoControls({
|
function VideoControls({
|
||||||
player,
|
|
||||||
enterFullscreen,
|
enterFullscreen,
|
||||||
|
toggleMuted,
|
||||||
|
togglePlayback,
|
||||||
timeRemaining,
|
timeRemaining,
|
||||||
isMuted,
|
isPlaying,
|
||||||
}: {
|
}: {
|
||||||
player: VideoPlayer
|
|
||||||
enterFullscreen: () => void
|
enterFullscreen: () => void
|
||||||
|
toggleMuted: () => void
|
||||||
|
togglePlayback: () => void
|
||||||
timeRemaining: number
|
timeRemaining: number
|
||||||
isMuted: boolean
|
isPlaying: boolean
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {muted} = useVideoVolumeState()
|
||||||
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:
|
// show countdown when:
|
||||||
// 1. timeRemaining is a number - was seeing NaNs
|
// 1. timeRemaining is a number - was seeing NaNs
|
||||||
@@ -140,14 +138,55 @@ function VideoControls({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.absolute, a.inset_0]}>
|
<View style={[a.absolute, a.inset_0]}>
|
||||||
{showTime && <TimeIndicator time={timeRemaining} />}
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPressFullscreen}
|
onPress={enterFullscreen}
|
||||||
style={a.flex_1}
|
style={a.flex_1}
|
||||||
accessibilityLabel={_(msg`Video`)}
|
accessibilityLabel={_(msg`Video`)}
|
||||||
accessibilityHint={_(msg`Tap to enter full screen`)}
|
accessibilityHint={_(msg`Tap to enter full screen`)}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
/>
|
/>
|
||||||
|
<ControlButton
|
||||||
|
onPress={togglePlayback}
|
||||||
|
label={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
|
||||||
|
accessibilityHint={_(msg`Tap to play or pause`)}
|
||||||
|
style={{left: 6}}>
|
||||||
|
{isPlaying ? (
|
||||||
|
<PauseIcon width={13} fill={t.palette.white} />
|
||||||
|
) : (
|
||||||
|
<PlayIcon width={13} fill={t.palette.white} />
|
||||||
|
)}
|
||||||
|
</ControlButton>
|
||||||
|
{showTime && <TimeIndicator time={timeRemaining} style={{left: 33}} />}
|
||||||
|
|
||||||
|
<ControlButton
|
||||||
|
onPress={toggleMuted}
|
||||||
|
label={muted ? _(msg`Unmute`) : _(msg`Mute`)}
|
||||||
|
accessibilityHint={_(msg`Tap to toggle sound`)}
|
||||||
|
style={{right: 6}}>
|
||||||
|
{muted ? (
|
||||||
|
<MuteIcon width={13} fill={t.palette.white} />
|
||||||
|
) : (
|
||||||
|
<UnmuteIcon width={13} fill={t.palette.white} />
|
||||||
|
)}
|
||||||
|
</ControlButton>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ControlButton({
|
||||||
|
onPress,
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
accessibilityHint,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
onPress: () => void
|
||||||
|
children: React.ReactNode
|
||||||
|
label: string
|
||||||
|
accessibilityHint: string
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInDown.duration(300)}
|
entering={FadeInDown.duration(300)}
|
||||||
style={[
|
style={[
|
||||||
@@ -159,25 +198,20 @@ function VideoControls({
|
|||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
bottom: 6,
|
bottom: 6,
|
||||||
right: 6,
|
|
||||||
minHeight: 21,
|
minHeight: 21,
|
||||||
minWidth: 21,
|
minWidth: 21,
|
||||||
},
|
},
|
||||||
|
style,
|
||||||
]}>
|
]}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={toggleMuted}
|
onPress={onPress}
|
||||||
style={a.flex_1}
|
style={a.flex_1}
|
||||||
accessibilityLabel={isMuted ? _(msg`Muted`) : _(msg`Unmuted`)}
|
accessibilityLabel={label}
|
||||||
accessibilityHint={_(msg`Tap to toggle sound`)}
|
accessibilityHint={accessibilityHint}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
hitSlop={HITSLOP_30}>
|
hitSlop={HITSLOP_30}>
|
||||||
{isMuted ? (
|
{children}
|
||||||
<MuteIcon width={13} fill={t.palette.white} />
|
|
||||||
) : (
|
|
||||||
<UnmuteIcon width={13} fill={t.palette.white} />
|
|
||||||
)}
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Context = React.createContext(
|
||||||
|
{} as {
|
||||||
|
muted: boolean
|
||||||
|
setMuted: (muted: boolean) => void
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
|
const [muted, setMuted] = React.useState(true)
|
||||||
|
|
||||||
|
const value = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
muted,
|
||||||
|
setMuted,
|
||||||
|
}),
|
||||||
|
[muted, setMuted],
|
||||||
|
)
|
||||||
|
|
||||||
|
return <Context.Provider value={value}>{children}</Context.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVideoVolumeState() {
|
||||||
|
const context = React.useContext(Context)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useVideoVolumeState must be used within a VideoVolumeProvider',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -4104,6 +4104,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||||
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||||
|
|
||||||
|
"@haileyok/bluesky-video@0.1.8":
|
||||||
|
version "0.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.8.tgz#26fc6ec64993d593e7a0ecb96febff96c5037ebd"
|
||||||
|
integrity sha512-jMtGSMU5jpacLvAFRWGC5gVvVKuKHrGH3gluz9MsBEbSRvS8bb9FhNtC1VHb+A4UUgLufOyzrSbDw9fa8hR8lg==
|
||||||
|
|
||||||
"@hapi/accept@^6.0.3":
|
"@hapi/accept@^6.0.3":
|
||||||
version "6.0.3"
|
version "6.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-6.0.3.tgz#eef0800a4f89cd969da8e5d0311dc877c37279ab"
|
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-6.0.3.tgz#eef0800a4f89cd969da8e5d0311dc877c37279ab"
|
||||||
@@ -12422,10 +12427,6 @@ expo-updates@~0.25.14:
|
|||||||
ignore "^5.3.1"
|
ignore "^5.3.1"
|
||||||
resolve-from "^5.0.0"
|
resolve-from "^5.0.0"
|
||||||
|
|
||||||
"expo-video@https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz":
|
|
||||||
version "1.2.4"
|
|
||||||
resolved "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz#4127dd5cea5fdf7ab745104c73b8ecf5506f5d34"
|
|
||||||
|
|
||||||
expo-web-browser@~13.0.3:
|
expo-web-browser@~13.0.3:
|
||||||
version "13.0.3"
|
version "13.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-13.0.3.tgz#dceb05dbc187b498ca937b02adf385b0232a4e92"
|
resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-13.0.3.tgz#dceb05dbc187b498ca937b02adf385b0232a4e92"
|
||||||
|
|||||||
Reference in New Issue
Block a user