From 8860890a8588bc3768a5146abee9510127cc70ed Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 4 Sep 2024 14:41:42 +0100 Subject: [PATCH 01/21] Don't log extra background events (#5134) --- src/lib/statsig/statsig.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 81707d2bf5..7d86f4078c 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -226,11 +226,11 @@ AppState.addEventListener('change', (state: AppStateStatus) => { let secondsActive = 0 if (lastActive != null) { secondsActive = Math.round((performance.now() - lastActive) / 1e3) + lastActive = null + logEvent('state:background:sampled', { + secondsActive, + }) } - lastActive = null - logEvent('state:background:sampled', { - secondsActive, - }) } }) From e2a244b99889743a8788b0c464d3e150bc8047ad Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 4 Sep 2024 14:42:22 +0100 Subject: [PATCH 02/21] Disable in-thread deduping for reposted replies (#5135) --- src/lib/api/feed-manip.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index d81f250b80..eaa760b4be 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -271,7 +271,12 @@ export class FeedTuner { } } else { if (!dryRun) { - this.seenUris.add(item.post.uri) + // Reposting a reply elevates it to top-level, so its parent/root won't be displayed. + // Disable in-thread dedupe for this case since we don't want to miss them later. + const disableDedupe = slice.isReply && slice.isRepost + if (!disableDedupe) { + this.seenUris.add(item.post.uri) + } } } } From 3eef62d995522524739b609b7d182ddb6b0cedd6 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 4 Sep 2024 15:29:20 +0100 Subject: [PATCH 03/21] log errors (#5139) --- src/state/queries/video/video.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/state/queries/video/video.ts b/src/state/queries/video/video.ts index ee07244980..0c65e22603 100644 --- a/src/state/queries/video/video.ts +++ b/src/state/queries/video/video.ts @@ -123,12 +123,16 @@ export function useUploadVideo({ blobRef, }) }, - onError: useCallback(() => { - dispatch({ - type: 'SetError', - error: _(msg`Video failed to process`), - }) - }, [_]), + onError: useCallback( + error => { + logger.error('Error processing video', {safeMessage: error}) + dispatch({ + type: 'SetError', + error: _(msg`Video failed to process`), + }) + }, + [_], + ), }) const {mutate: onVideoCompressed} = useUploadVideoMutation({ @@ -140,6 +144,7 @@ export function useUploadVideo({ setJobId(response.jobId) }, onError: e => { + logger.error('Error uploading video', {safeMessage: e}) if (e instanceof ServerError) { dispatch({ type: 'SetError', @@ -171,6 +176,7 @@ export function useUploadVideo({ onVideoCompressed(video) }, onError: e => { + logger.error('Error uploading video', {safeMessage: e}) if (e instanceof VideoTooLargeError) { dispatch({ type: 'SetError', From 515f87ed2487d6d875dfc6a266e47e7785e94818 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 4 Sep 2024 15:56:29 +0100 Subject: [PATCH 04/21] fail video if cannot load preview (#5138) --- src/view/com/composer/videos/VideoPreview.web.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/view/com/composer/videos/VideoPreview.web.tsx b/src/view/com/composer/videos/VideoPreview.web.tsx index e802adddfb..4c44781cfb 100644 --- a/src/view/com/composer/videos/VideoPreview.web.tsx +++ b/src/view/com/composer/videos/VideoPreview.web.tsx @@ -1,9 +1,12 @@ import React, {useEffect, useRef} from 'react' import {View} from 'react-native' import {ImagePickerAsset} from 'expo-image-picker' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {CompressedVideo} from '#/lib/media/video/types' import {clamp} from '#/lib/numbers' +import * as Toast from '#/view/com/util/Toast' import {ExternalEmbedRemoveBtn} from 'view/com/composer/ExternalEmbedRemoveBtn' import {atoms as a} from '#/alf' @@ -19,6 +22,7 @@ export function VideoPreview({ clear: () => void }) { const ref = useRef(null) + const {_} = useLingui() useEffect(() => { if (!ref.current) return @@ -32,11 +36,19 @@ export function VideoPreview({ }, {signal}, ) + ref.current.addEventListener( + 'error', + () => { + Toast.show(_(msg`Could not process your video`)) + clear() + }, + {signal}, + ) return () => { abortController.abort() } - }, [setDimensions]) + }, [setDimensions, _, clear]) let aspectRatio = asset.width / asset.height From 21e48bb2d80a5becf3ffdecb1415322e7eae3f14 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 4 Sep 2024 08:00:53 -0700 Subject: [PATCH 05/21] [Video] Tweak playback handling (#5127) --- src/view/com/util/post-embeds/VideoEmbed.tsx | 31 +++++++++++++------ .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 5 ++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx index 55ac188248..03838b6620 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -18,7 +18,8 @@ import * as VideoFallback from './VideoEmbedInner/VideoFallback' export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { const t = useTheme() - const {activeSource, setActiveSource} = useActiveVideoNative() + const {activeSource, setActiveSource, player} = useActiveVideoNative() + const [isFullscreen, setIsFullscreen] = React.useState(false) const isActive = embed.playlist === activeSource const {_} = useLingui() @@ -31,6 +32,20 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { ) const gate = useGate() + const onChangeStatus = (isVisible: boolean) => { + if (isVisible) { + setActiveSource(embed.playlist) + if (!player.playing) { + player.play() + } + } else if (!isFullscreen) { + player.muted = true + if (player.playing) { + player.pause() + } + } + } + if (!gate('video_view_on_posts')) { return null } @@ -54,15 +69,13 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { a.my_xs, ]}> - { - if (isVisible) { - setActiveSource(embed.playlist) - } - }}> + {isActive ? ( - + ) : ( <> void }) { const {_} = useLingui() const {player} = useActiveVideoNative() const ref = useRef(null) - const [isFullscreen, setIsFullscreen] = useState(false) const enterFullscreen = useCallback(() => { ref.current?.enterFullscreen() From dee28f378a815e6518a010a293733b26ae7bed9c Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 4 Sep 2024 08:06:45 -0700 Subject: [PATCH 06/21] [Video] Only allow one `VideoView` to be active at a time, regardless of source (#5131) --- .../post-embeds/ActiveVideoNativeContext.tsx | 19 ++++++++++++++++--- src/view/com/util/post-embeds/VideoEmbed.tsx | 17 ++++++++++------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx b/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx index 77616d7880..bdc7967cba 100644 --- a/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx +++ b/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx @@ -4,8 +4,9 @@ import {useVideoPlayer, VideoPlayer} from 'expo-video' import {isNative} from '#/platform/detection' const Context = React.createContext<{ - activeSource: string | null - setActiveSource: (src: string) => void + activeSource: string + activeViewId: string | undefined + setActiveSource: (src: string, viewId: string) => void player: VideoPlayer } | null>(null) @@ -15,6 +16,7 @@ export function Provider({children}: {children: React.ReactNode}) { } const [activeSource, setActiveSource] = React.useState('') + const [activeViewId, setActiveViewId] = React.useState() const player = useVideoPlayer(activeSource, p => { p.muted = true @@ -22,8 +24,19 @@ export function Provider({children}: {children: React.ReactNode}) { p.play() }) + const setActiveSourceOuter = (src: string, viewId: string) => { + setActiveSource(src) + setActiveViewId(viewId) + } + return ( - + {children} ) diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx index 03838b6620..988ba57390 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react' +import React, {useCallback, useId, useState} from 'react' import {View} from 'react-native' import {Image} from 'expo-image' import {AppBskyEmbedVideo} from '@atproto/api' @@ -17,11 +17,14 @@ import {useActiveVideoNative} from './ActiveVideoNativeContext' import * as VideoFallback from './VideoEmbedInner/VideoFallback' export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { - const t = useTheme() - const {activeSource, setActiveSource, player} = useActiveVideoNative() - const [isFullscreen, setIsFullscreen] = React.useState(false) - const isActive = embed.playlist === activeSource const {_} = useLingui() + const t = useTheme() + const {activeSource, activeViewId, setActiveSource, player} = + useActiveVideoNative() + const viewId = useId() + + const [isFullscreen, setIsFullscreen] = React.useState(false) + const isActive = embed.playlist === activeSource && activeViewId === viewId const [key, setKey] = useState(0) const renderError = useCallback( @@ -34,7 +37,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { const onChangeStatus = (isVisible: boolean) => { if (isVisible) { - setActiveSource(embed.playlist) + setActiveSource(embed.playlist, viewId) if (!player.playing) { player.play() } @@ -88,7 +91,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { )} From c36c47d49aff74f8a4176db0db6a13de141f5d95 Mon Sep 17 00:00:00 2001 From: Marco Buono Date: Wed, 4 Sep 2024 15:04:08 -0300 Subject: [PATCH 13/21] Add slight spacing between Post and CW button (#5125) --- src/view/com/composer/Composer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index d8aa598eff..a6eadd251a 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -598,7 +598,7 @@ export const ComposePost = observer(function ComposePost({ ) : ( - <> + )} - + )} @@ -1002,6 +1002,10 @@ const styles = StyleSheet.create({ paddingVertical: 6, marginLeft: 12, }, + postBtnWrapper: { + flexDirection: 'row', + gap: 14, + }, errorLine: { flexDirection: 'row', alignItems: 'center', From e8eaf2f4a72f9c2c45299637425ebc299079caf0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 4 Sep 2024 19:42:28 +0100 Subject: [PATCH 14/21] allow only posting video (#5142) --- src/view/com/composer/Composer.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index a6eadd251a..6a6ac72674 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -224,7 +224,12 @@ export const ComposePost = observer(function ComposePost({ ) const onPressCancel = useCallback(() => { - if (graphemeLength > 0 || !gallery.isEmpty || extGif) { + if ( + graphemeLength > 0 || + !gallery.isEmpty || + extGif || + videoUploadState.status !== 'idle' + ) { closeAllDialogs() Keyboard.dismiss() discardPromptControl.open() @@ -238,6 +243,7 @@ export const ComposePost = observer(function ComposePost({ closeAllDialogs, discardPromptControl, onClose, + videoUploadState.status, ]) useImperativeHandle(cancelRef, () => ({onPressCancel})) @@ -332,7 +338,8 @@ export const ComposePost = observer(function ComposePost({ richtext.text.trim().length === 0 && gallery.isEmpty && !extLink && - !quote + !quote && + videoUploadState.status === 'idle' ) { setError(_(msg`Did you want to say anything?`)) return From fcf27f05127d6e59f66ba0af745179e4b6653f77 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 4 Sep 2024 19:56:02 +0100 Subject: [PATCH 15/21] [Video] content fit cover on native (#5140) --- src/view/com/util/post-embeds/VideoEmbed.tsx | 2 +- .../util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx index d8410951a3..a5bc97f854 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -84,7 +84,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { source={{uri: embed.thumbnail}} alt={embed.alt} style={a.flex_1} - contentFit="contain" + contentFit="cover" accessibilityIgnoresInvertColors /> - - )} - + ) } +function InnerWrapper({embed}: Props) { + const {_} = useLingui() + const {activeSource, activeViewId, setActiveSource, player} = + useActiveVideoNative() + const viewId = useId() + + const [playerStatus, setPlayerStatus] = useState('loading') + const [isMuted, setIsMuted] = useState(player.muted) + const [isFullscreen, setIsFullscreen] = React.useState(false) + const [timeRemaining, setTimeRemaining] = React.useState(0) + const isActive = embed.playlist === activeSource && activeViewId === viewId + const isLoading = + isActive && + (playerStatus === 'waitingToPlayAtSpecifiedRate' || + playerStatus === 'loading') + + 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( + 'statusChange', + (status, _oldStatus, error) => { + setPlayerStatus(status) + if (status === 'error') { + throw error + } + }, + ) + return () => { + volumeSub.remove() + timeSub.remove() + statusSub.remove() + } + } + }, [player, isActive]) + + useEffect(() => { + if (!isActive && playerStatus !== 'loading') { + setPlayerStatus('loading') + } + }, [isActive, playerStatus]) + + const onChangeStatus = (isVisible: boolean) => { + if (isFullscreen) { + return + } + + if (isVisible) { + setActiveSource(embed.playlist, viewId) + if (!player.playing) { + player.play() + } + } else { + player.muted = true + if (player.playing) { + player.pause() + } + } + } + + return ( + + {isActive ? ( + + ) : null} + {!isActive || isLoading ? ( + + {embed.alt} + + + ) : null} + + ) +} + function VideoError({retry}: {error: unknown; retry: () => void}) { return ( diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx index 4fafce1de2..3fa159267d 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react' +import React, {useCallback, useRef} from 'react' import {Pressable, View} from 'react-native' import Animated, {FadeInDown} from 'react-native-reanimated' import {VideoPlayer, VideoView} from 'expo-video' @@ -22,10 +22,14 @@ export function VideoEmbedInnerNative({ embed, isFullscreen, setIsFullscreen, + isMuted, + timeRemaining, }: { embed: AppBskyEmbedVideo.View isFullscreen: boolean setIsFullscreen: (isFullscreen: boolean) => void + timeRemaining: number + isMuted: boolean }) { const {_} = useLingui() const {player} = useActiveVideoNative() @@ -73,7 +77,12 @@ export function VideoEmbedInnerNative({ } accessibilityHint="" /> - + ) } @@ -81,40 +90,16 @@ export function VideoEmbedInnerNative({ function VideoControls({ player, enterFullscreen, + timeRemaining, + isMuted, }: { player: VideoPlayer enterFullscreen: () => void + timeRemaining: number + isMuted: boolean }) { const {_} = useLingui() const t = useTheme() - const [isMuted, setIsMuted] = useState(player.muted) - const [timeRemaining, setTimeRemaining] = React.useState(0) - - useEffect(() => { - // 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( - 'statusChange', - (status, _oldStatus, error) => { - if (status === 'error') { - throw error - } - }, - ) - return () => { - volumeSub.remove() - timeSub.remove() - statusSub.remove() - } - }, [player]) const onPressFullscreen = useCallback(() => { switch (player.status) {