Merge branch 'main' into hailey/device-attestation
This commit is contained in:
+1
-1
@@ -142,7 +142,7 @@
|
||||
"expo-file-system": "~18.1.10",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image": "~2.2.1",
|
||||
"expo-image": "^2.4.0",
|
||||
"expo-image-crop-tool": "^0.1.8",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useRef} from 'react'
|
||||
import {useImperativeHandle, useRef, useState} from 'react'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||
@@ -17,91 +17,88 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {TimeIndicator} from './TimeIndicator'
|
||||
|
||||
export const VideoEmbedInnerNative = React.forwardRef(
|
||||
function VideoEmbedInnerNative(
|
||||
{
|
||||
embed,
|
||||
setStatus,
|
||||
setIsLoading,
|
||||
setIsActive,
|
||||
}: {
|
||||
embed: AppBskyEmbedVideo.View
|
||||
setStatus: (status: 'playing' | 'paused') => void
|
||||
setIsLoading: (isLoading: boolean) => void
|
||||
setIsActive: (isActive: boolean) => void
|
||||
export function VideoEmbedInnerNative({
|
||||
ref,
|
||||
embed,
|
||||
setStatus,
|
||||
setIsLoading,
|
||||
setIsActive,
|
||||
}: {
|
||||
ref: React.Ref<{togglePlayback: () => void}>
|
||||
embed: AppBskyEmbedVideo.View
|
||||
setStatus: (status: 'playing' | 'paused') => void
|
||||
setIsLoading: (isLoading: boolean) => void
|
||||
setIsActive: (isActive: boolean) => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const videoRef = useRef<BlueskyVideoView>(null)
|
||||
const autoplayDisabled = useAutoplayDisabled()
|
||||
const isWithinMessage = useIsWithinMessage()
|
||||
const [muted, setMuted] = useVideoMuteState()
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [timeRemaining, setTimeRemaining] = useState(0)
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
togglePlayback: () => {
|
||||
videoRef.current?.togglePlayback()
|
||||
},
|
||||
ref: React.Ref<{togglePlayback: () => void}>,
|
||||
) {
|
||||
const {_} = useLingui()
|
||||
const videoRef = useRef<BlueskyVideoView>(null)
|
||||
const autoplayDisabled = useAutoplayDisabled()
|
||||
const isWithinMessage = useIsWithinMessage()
|
||||
const [muted, setMuted] = useVideoMuteState()
|
||||
}))
|
||||
|
||||
const [isPlaying, setIsPlaying] = React.useState(false)
|
||||
const [timeRemaining, setTimeRemaining] = React.useState(0)
|
||||
const [error, setError] = React.useState<string>()
|
||||
if (error) {
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
togglePlayback: () => {
|
||||
videoRef.current?.togglePlayback()
|
||||
},
|
||||
}))
|
||||
|
||||
if (error) {
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, a.relative]}>
|
||||
<BlueskyVideoView
|
||||
url={embed.playlist}
|
||||
autoplay={!autoplayDisabled && !isWithinMessage}
|
||||
beginMuted={autoplayDisabled ? false : muted}
|
||||
style={[a.rounded_sm]}
|
||||
onActiveChange={e => {
|
||||
setIsActive(e.nativeEvent.isActive)
|
||||
}}
|
||||
onLoadingChange={e => {
|
||||
setIsLoading(e.nativeEvent.isLoading)
|
||||
}}
|
||||
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={
|
||||
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
|
||||
}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
<VideoControls
|
||||
enterFullscreen={() => {
|
||||
videoRef.current?.enterFullscreen(true)
|
||||
}}
|
||||
toggleMuted={() => {
|
||||
videoRef.current?.toggleMuted()
|
||||
}}
|
||||
togglePlayback={() => {
|
||||
videoRef.current?.togglePlayback()
|
||||
}}
|
||||
isPlaying={isPlaying}
|
||||
timeRemaining={timeRemaining}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<View style={[a.flex_1, a.relative]}>
|
||||
<BlueskyVideoView
|
||||
url={embed.playlist}
|
||||
autoplay={!autoplayDisabled && !isWithinMessage}
|
||||
beginMuted={autoplayDisabled ? false : muted}
|
||||
style={[a.rounded_sm]}
|
||||
onActiveChange={e => {
|
||||
setIsActive(e.nativeEvent.isActive)
|
||||
}}
|
||||
onLoadingChange={e => {
|
||||
setIsLoading(e.nativeEvent.isLoading)
|
||||
}}
|
||||
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={
|
||||
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
|
||||
}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
<VideoControls
|
||||
enterFullscreen={() => {
|
||||
videoRef.current?.enterFullscreen(true)
|
||||
}}
|
||||
toggleMuted={() => {
|
||||
videoRef.current?.toggleMuted()
|
||||
}}
|
||||
togglePlayback={() => {
|
||||
videoRef.current?.togglePlayback()
|
||||
}}
|
||||
isPlaying={isPlaying}
|
||||
timeRemaining={timeRemaining}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function VideoControls({
|
||||
enterFullscreen,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {useCallback, useRef, useState} from 'react'
|
||||
import {ActivityIndicator, View} from 'react-native'
|
||||
import {ImageBackground} from 'expo-image'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
@@ -81,13 +81,13 @@ export function VideoEmbed({embed, crop}: Props) {
|
||||
|
||||
function InnerWrapper({embed}: Props) {
|
||||
const {_} = useLingui()
|
||||
const ref = React.useRef<{togglePlayback: () => void}>(null)
|
||||
const ref = useRef<{togglePlayback: () => void}>(null)
|
||||
|
||||
const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>(
|
||||
const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>(
|
||||
'pending',
|
||||
)
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [isActive, setIsActive] = React.useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isActive, setIsActive] = useState(false)
|
||||
const showSpinner = useThrottledValue(isActive && isLoading, 100)
|
||||
|
||||
const showOverlay =
|
||||
@@ -96,11 +96,9 @@ function InnerWrapper({embed}: Props) {
|
||||
(status === 'paused' && !isActive) ||
|
||||
status === 'pending'
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isActive && status !== 'pending') {
|
||||
setStatus('pending')
|
||||
}
|
||||
}, [isActive, status])
|
||||
if (!isActive && status !== 'pending') {
|
||||
setStatus('pending')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -131,8 +129,7 @@ function InnerWrapper({embed}: Props) {
|
||||
onPress={() => {
|
||||
ref.current?.togglePlayback()
|
||||
}}
|
||||
label={_(msg`Play video`)}
|
||||
color="secondary">
|
||||
label={_(msg`Play video`)}>
|
||||
{showSpinner ? (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
@@ -83,9 +90,7 @@ export function VideoEmbed({
|
||||
style={{display: 'flex', flex: 1, cursor: 'default'}}
|
||||
onClick={evt => evt.stopPropagation()}>
|
||||
<ErrorBoundary renderError={renderError} key={key}>
|
||||
<ViewportObserver
|
||||
sendPosition={sendPosition}
|
||||
isAnyViewActive={currentActiveView !== null}>
|
||||
<OnlyNearScreen>
|
||||
<VideoEmbedInnerWeb
|
||||
embed={embed}
|
||||
active={active}
|
||||
@@ -93,31 +98,39 @@ export function VideoEmbed({
|
||||
onScreen={onScreen}
|
||||
lastKnownTime={lastKnownTime}
|
||||
/>
|
||||
</ViewportObserver>
|
||||
</OnlyNearScreen>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[a.pt_xs]}>
|
||||
{cropDisabled ? (
|
||||
<View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
|
||||
{contents}
|
||||
</View>
|
||||
) : (
|
||||
<ConstrainedImage
|
||||
fullBleed={crop === 'square'}
|
||||
aspectRatio={constrained || 1}>
|
||||
{contents}
|
||||
</ConstrainedImage>
|
||||
)}
|
||||
<ViewportObserver
|
||||
sendPosition={sendPosition}
|
||||
isAnyViewActive={currentActiveView !== null}>
|
||||
{cropDisabled ? (
|
||||
<View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
|
||||
{contents}
|
||||
</View>
|
||||
) : (
|
||||
<ConstrainedImage
|
||||
fullBleed={crop === 'square'}
|
||||
aspectRatio={constrained || 1}>
|
||||
{contents}
|
||||
</ConstrainedImage>
|
||||
)}
|
||||
</ViewportObserver>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const NearScreenContext = createContext(false)
|
||||
|
||||
/**
|
||||
* Renders a 100vh tall div and watches it with an IntersectionObserver to
|
||||
* send the position of the div when it's near the screen.
|
||||
*
|
||||
* IMPORTANT: ViewportObserver _must_ not be within a `overflow: hidden` container.
|
||||
*/
|
||||
function ViewportObserver({
|
||||
children,
|
||||
@@ -164,7 +177,9 @@ function ViewportObserver({
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, a.flex_row]}>
|
||||
{nearScreen && children}
|
||||
<NearScreenContext.Provider value={nearScreen}>
|
||||
{children}
|
||||
</NearScreenContext.Provider>
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
@@ -182,6 +197,18 @@ function ViewportObserver({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Awkward data flow here, but we need to hide the video when it's not near the screen.
|
||||
* But also, ViewportObserver _must_ not be within a `overflow: hidden` container.
|
||||
* So we put it at the top level of the component tree here, then hide the children of
|
||||
* the auto-resizing container.
|
||||
*/
|
||||
export const OnlyNearScreen = ({children}: {children: React.ReactNode}) => {
|
||||
const nearScreen = useContext(NearScreenContext)
|
||||
|
||||
return nearScreen ? children : null
|
||||
}
|
||||
|
||||
function VideoError({error, retry}: {error: unknown; retry: () => void}) {
|
||||
const {_} = useLingui()
|
||||
|
||||
|
||||
@@ -78,7 +78,12 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
|
||||
<Toast content={activeToast.content} type={activeToast.type} />
|
||||
<Pressable
|
||||
style={[a.absolute, a.inset_0]}
|
||||
accessibilityLabel={_(msg`Dismiss toast`)}
|
||||
accessibilityLabel={_(
|
||||
msg({
|
||||
message: `Dismiss message`,
|
||||
comment: `Accessibility label for dismissing a toast notification`,
|
||||
}),
|
||||
)}
|
||||
accessibilityHint=""
|
||||
onPress={() => setActiveToast(undefined)}
|
||||
/>
|
||||
|
||||
@@ -41,6 +41,7 @@ export class FeedViewPostsSlice {
|
||||
isIncompleteThread: boolean
|
||||
isFallbackMarker: boolean
|
||||
isOrphan: boolean
|
||||
isThreadMuted: boolean
|
||||
rootUri: string
|
||||
feedPostUri: string
|
||||
|
||||
@@ -50,6 +51,7 @@ export class FeedViewPostsSlice {
|
||||
this.isIncompleteThread = false
|
||||
this.isFallbackMarker = false
|
||||
this.isOrphan = false
|
||||
this.isThreadMuted = post.viewer?.threadMuted ?? false
|
||||
this.feedPostUri = post.uri
|
||||
if (AppBskyFeedDefs.isPostView(reply?.root)) {
|
||||
this.rootUri = reply.root.uri
|
||||
@@ -361,6 +363,20 @@ export class FeedTuner {
|
||||
return slices
|
||||
}
|
||||
|
||||
static removeMutedThreads(
|
||||
tuner: FeedTuner,
|
||||
slices: FeedViewPostsSlice[],
|
||||
_dryRun: boolean,
|
||||
) {
|
||||
for (let i = 0; i < slices.length; i++) {
|
||||
if (slices[i].isThreadMuted) {
|
||||
slices.splice(i, 1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
return slices
|
||||
}
|
||||
|
||||
static dedupThreads(
|
||||
tuner: FeedTuner,
|
||||
slices: FeedViewPostsSlice[],
|
||||
|
||||
@@ -400,9 +400,7 @@ export function getNotificationPayload(
|
||||
}
|
||||
}
|
||||
|
||||
export function notificationToURL(
|
||||
payload: NotificationPayload,
|
||||
): string | undefined {
|
||||
export function notificationToURL(payload: NotificationPayload): string | null {
|
||||
switch (payload?.reason) {
|
||||
case 'like':
|
||||
case 'repost':
|
||||
@@ -433,10 +431,12 @@ export function notificationToURL(
|
||||
}
|
||||
case 'chat-message':
|
||||
// should be handled separately
|
||||
return undefined
|
||||
return null
|
||||
case 'verified':
|
||||
case 'unverified':
|
||||
default:
|
||||
return '/notifications'
|
||||
default:
|
||||
// do nothing if we don't know what to do with it
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useMemo} from 'react'
|
||||
|
||||
import {FeedTuner} from '#/lib/api/feed-manip'
|
||||
import {FeedDescriptor} from '../queries/post-feed'
|
||||
import {type FeedDescriptor} from '../queries/post-feed'
|
||||
import {usePreferencesQuery} from '../queries/preferences'
|
||||
import {useSession} from '../session'
|
||||
import {useLanguagePrefs} from './languages'
|
||||
@@ -19,7 +19,10 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
|
||||
}
|
||||
}
|
||||
if (feedDesc.startsWith('feedgen')) {
|
||||
return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)]
|
||||
return [
|
||||
FeedTuner.preferredLangOnly(langPrefs.contentLanguages),
|
||||
FeedTuner.removeMutedThreads,
|
||||
]
|
||||
}
|
||||
if (feedDesc === 'following' || feedDesc.startsWith('list')) {
|
||||
const feedTuners = [FeedTuner.removeOrphans]
|
||||
@@ -40,6 +43,7 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
|
||||
feedTuners.push(FeedTuner.removeQuotePosts)
|
||||
}
|
||||
feedTuners.push(FeedTuner.dedupThreads)
|
||||
feedTuners.push(FeedTuner.removeMutedThreads)
|
||||
|
||||
return feedTuners
|
||||
}
|
||||
|
||||
@@ -11301,10 +11301,10 @@ expo-image-picker@~16.1.4:
|
||||
dependencies:
|
||||
expo-image-loader "~5.1.0"
|
||||
|
||||
expo-image@~2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.2.1.tgz#b4aa706a25f7e8902ac854a8da249caf4a90cd67"
|
||||
integrity sha512-5ZSggMi0X2G9AN0aM+sdkCyyZ6YcWvGs9KYLYrRBVUN3ph6RBiu6mKGpaNN1TAscySRnH1eHbUE1H+Qeq7qm1g==
|
||||
expo-image@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.4.0.tgz#02f7fd743387206914cd431a6367f5be53509e3e"
|
||||
integrity sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw==
|
||||
|
||||
expo-json-utils@~0.15.0:
|
||||
version "0.15.0"
|
||||
|
||||
Reference in New Issue
Block a user