Merge branch 'main' into hailey/device-attestation

This commit is contained in:
Hailey
2025-08-04 18:41:28 -07:00
9 changed files with 171 additions and 125 deletions
+1 -1
View File
@@ -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,
+9 -12
View File
@@ -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()
+6 -1
View File
@@ -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)}
/>
+16
View File
@@ -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[],
+5 -5
View File
@@ -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
}
}
+6 -2
View File
@@ -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
}
+4 -4
View File
@@ -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"