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