From 3ee5ef32d9d4342c3ce473933d84aa2ef01dd97b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 3 Sep 2024 22:49:19 +0100 Subject: [PATCH 01/76] [Video] Error handling in composer, fix auto-send (#5122) * tweak * error state for upload toolbar * catch errors in upload status query * stop query on error --------- Co-authored-by: Hailey --- src/state/queries/video/video.ts | 39 +++- src/view/com/composer/Composer.tsx | 314 +++++++++++++++++------------ 2 files changed, 209 insertions(+), 144 deletions(-) diff --git a/src/state/queries/video/video.ts b/src/state/queries/video/video.ts index 5e36ce35..ee072449 100644 --- a/src/state/queries/video/video.ts +++ b/src/state/queries/video/video.ts @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react' +import React, {useCallback, useEffect} from 'react' import {ImagePickerAsset} from 'expo-image-picker' import {AppBskyVideoDefs, BlobRef} from '@atproto/api' import {msg} from '@lingui/macro' @@ -25,7 +25,7 @@ type Action = | {type: 'SetDimensions'; width: number; height: number} | {type: 'SetVideo'; video: CompressedVideo} | {type: 'SetJobStatus'; jobStatus: AppBskyVideoDefs.JobStatus} - | {type: 'SetBlobRef'; blobRef: BlobRef} + | {type: 'SetComplete'; blobRef: BlobRef} export interface State { status: Status @@ -36,6 +36,7 @@ export interface State { blobRef?: BlobRef error?: string abortController: AbortController + pendingPublish?: {blobRef: BlobRef; mutableProcessed: boolean} } function reducer(queryClient: QueryClient) { @@ -77,8 +78,15 @@ function reducer(queryClient: QueryClient) { updatedState = {...state, video: action.video, status: 'uploading'} } else if (action.type === 'SetJobStatus') { updatedState = {...state, jobStatus: action.jobStatus} - } else if (action.type === 'SetBlobRef') { - updatedState = {...state, blobRef: action.blobRef, status: 'done'} + } else if (action.type === 'SetComplete') { + updatedState = { + ...state, + pendingPublish: { + blobRef: action.blobRef, + mutableProcessed: false, + }, + status: 'done', + } } return updatedState } @@ -86,7 +94,6 @@ function reducer(queryClient: QueryClient) { export function useUploadVideo({ setStatus, - onSuccess, }: { setStatus: (status: string) => void onSuccess: () => void @@ -112,11 +119,16 @@ export function useUploadVideo({ }, onSuccess: blobRef => { dispatch({ - type: 'SetBlobRef', + type: 'SetComplete', blobRef, }) - onSuccess() }, + onError: useCallback(() => { + dispatch({ + type: 'SetError', + error: _(msg`Video failed to process`), + }) + }, [_]), }) const {mutate: onVideoCompressed} = useUploadVideoMutation({ @@ -215,15 +227,17 @@ export function useUploadVideo({ const useUploadStatusQuery = ({ onStatusChange, onSuccess, + onError, }: { onStatusChange: (status: AppBskyVideoDefs.JobStatus) => void onSuccess: (blobRef: BlobRef) => void + onError: (error: Error) => void }) => { const videoAgent = useVideoAgent() const [enabled, setEnabled] = React.useState(true) const [jobId, setJobId] = React.useState() - const {isLoading, isError} = useQuery({ + const {error} = useQuery({ queryKey: ['video', 'upload status', jobId], queryFn: async () => { if (!jobId) return // this won't happen, can ignore @@ -245,9 +259,14 @@ const useUploadStatusQuery = ({ refetchInterval: 1500, }) + useEffect(() => { + if (error) { + onError(error) + setEnabled(false) + } + }, [error, onError]) + return { - isLoading, - isError, setJobId: (_jobId: string) => { setJobId(_jobId) setEnabled(true) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index e42e23ba..b07adf2a 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -190,6 +190,7 @@ export const ComposePost = observer(function ComposePost({ } }, }) + const [publishOnUpload, setPublishOnUpload] = useState(false) const {extLink, setExtLink} = useExternalLinkFetch({setQuote, setError}) @@ -303,147 +304,187 @@ export const ComposePost = observer(function ComposePost({ return false }, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled]) - const onPressPublish = async (finishedUploading?: boolean) => { - if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { - return - } + const onPressPublish = React.useCallback( + async (finishedUploading?: boolean) => { + if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { + return + } - if (isAltTextRequiredAndMissing) { - return - } + if (isAltTextRequiredAndMissing) { + return + } - if ( - !finishedUploading && - videoUploadState.asset && - videoUploadState.status !== 'done' - ) { - setPublishOnUpload(true) - return - } + if ( + !finishedUploading && + videoUploadState.asset && + videoUploadState.status !== 'done' + ) { + setPublishOnUpload(true) + return + } - setError('') + setError('') - if ( - richtext.text.trim().length === 0 && - gallery.isEmpty && - !extLink && - !quote - ) { - setError(_(msg`Did you want to say anything?`)) - return - } - if (extLink?.isLoading) { - setError(_(msg`Please wait for your link card to finish loading`)) - return - } + if ( + richtext.text.trim().length === 0 && + gallery.isEmpty && + !extLink && + !quote + ) { + setError(_(msg`Did you want to say anything?`)) + return + } + if (extLink?.isLoading) { + setError(_(msg`Please wait for your link card to finish loading`)) + return + } - setIsProcessing(true) + setIsProcessing(true) - let postUri - try { - postUri = ( - await apilib.post(agent, { - rawText: richtext.text, - replyTo: replyTo?.uri, - images: gallery.images, - quote, - extLink, - labels, - threadgate: threadgateAllowUISettings, - postgate, - onStateChange: setProcessingState, - langs: toPostLanguages(langPrefs.postLanguage), - video: videoUploadState.blobRef - ? { - blobRef: videoUploadState.blobRef, - altText: videoAltText, - captions: captions, - aspectRatio: videoUploadState.asset - ? { - width: videoUploadState.asset?.width, - height: videoUploadState.asset?.height, - } - : undefined, - } - : undefined, - }) - ).uri + let postUri try { - await whenAppViewReady(agent, postUri, res => { - const thread = res.data.thread - return AppBskyFeedDefs.isThreadViewPost(thread) - }) - } catch (waitErr: any) { - logger.error(waitErr, { - message: `Waiting for app view failed`, - }) - // Keep going because the post *was* published. - } - } catch (e: any) { - logger.error(e, { - message: `Composer: create post failed`, - hasImages: gallery.size > 0, - }) - - if (extLink) { - setExtLink({ - ...extLink, - isLoading: true, - localThumb: undefined, - } as apilib.ExternalEmbedDraft) - } - let err = cleanError(e.message) - if (err.includes('not locate record')) { - err = _( - msg`We're sorry! The post you are replying to has been deleted.`, - ) - } - setError(err) - setIsProcessing(false) - return - } finally { - if (postUri) { - logEvent('post:create', { - imageCount: gallery.size, - isReply: replyTo != null, - hasLink: extLink != null, - hasQuote: quote != null, - langs: langPrefs.postLanguage, - logContext: 'Composer', - }) - } - track('Create Post', { - imageCount: gallery.size, - }) - if (replyTo && replyTo.uri) track('Post:Reply') - } - if (postUri && !replyTo) { - emitPostCreated() - } - setLangPrefs.savePostLanguageToHistory() - if (quote) { - // We want to wait for the quote count to update before we call `onPost`, which will refetch data - whenAppViewReady(agent, quote.uri, res => { - const thread = res.data.thread - if ( - AppBskyFeedDefs.isThreadViewPost(thread) && - thread.post.quoteCount !== quoteCount - ) { - onPost?.(postUri) - return true + postUri = ( + await apilib.post(agent, { + rawText: richtext.text, + replyTo: replyTo?.uri, + images: gallery.images, + quote, + extLink, + labels, + threadgate: threadgateAllowUISettings, + postgate, + onStateChange: setProcessingState, + langs: toPostLanguages(langPrefs.postLanguage), + video: videoUploadState.pendingPublish?.blobRef + ? { + blobRef: videoUploadState.pendingPublish.blobRef, + altText: videoAltText, + captions: captions, + aspectRatio: videoUploadState.asset + ? { + width: videoUploadState.asset?.width, + height: videoUploadState.asset?.height, + } + : undefined, + } + : undefined, + }) + ).uri + try { + await whenAppViewReady(agent, postUri, res => { + const thread = res.data.thread + return AppBskyFeedDefs.isThreadViewPost(thread) + }) + } catch (waitErr: any) { + logger.error(waitErr, { + message: `Waiting for app view failed`, + }) + // Keep going because the post *was* published. } - return false - }) - } else { - onPost?.(postUri) + } catch (e: any) { + logger.error(e, { + message: `Composer: create post failed`, + hasImages: gallery.size > 0, + }) + + if (extLink) { + setExtLink({ + ...extLink, + isLoading: true, + localThumb: undefined, + } as apilib.ExternalEmbedDraft) + } + let err = cleanError(e.message) + if (err.includes('not locate record')) { + err = _( + msg`We're sorry! The post you are replying to has been deleted.`, + ) + } + setError(err) + setIsProcessing(false) + return + } finally { + if (postUri) { + logEvent('post:create', { + imageCount: gallery.size, + isReply: replyTo != null, + hasLink: extLink != null, + hasQuote: quote != null, + langs: langPrefs.postLanguage, + logContext: 'Composer', + }) + } + track('Create Post', { + imageCount: gallery.size, + }) + if (replyTo && replyTo.uri) track('Post:Reply') + } + if (postUri && !replyTo) { + emitPostCreated() + } + setLangPrefs.savePostLanguageToHistory() + if (quote) { + // We want to wait for the quote count to update before we call `onPost`, which will refetch data + whenAppViewReady(agent, quote.uri, res => { + const thread = res.data.thread + if ( + AppBskyFeedDefs.isThreadViewPost(thread) && + thread.post.quoteCount !== quoteCount + ) { + onPost?.(postUri) + return true + } + return false + }) + } else { + onPost?.(postUri) + } + onClose() + Toast.show( + replyTo + ? _(msg`Your reply has been published`) + : _(msg`Your post has been published`), + ) + }, + [ + _, + agent, + captions, + extLink, + gallery.images, + gallery.isEmpty, + gallery.size, + graphemeLength, + isAltTextRequiredAndMissing, + isProcessing, + labels, + langPrefs.postLanguage, + onClose, + onPost, + postgate, + quote, + quoteCount, + replyTo, + richtext.text, + setExtLink, + setLangPrefs, + threadgateAllowUISettings, + track, + videoAltText, + videoUploadState.asset, + videoUploadState.pendingPublish, + videoUploadState.status, + ], + ) + + React.useEffect(() => { + if (videoUploadState.pendingPublish && publishOnUpload) { + if (!videoUploadState.pendingPublish.mutableProcessed) { + videoUploadState.pendingPublish.mutableProcessed = true + onPressPublish(true) + } } - onClose() - Toast.show( - replyTo - ? _(msg`Your reply has been published`) - : _(msg`Your post has been published`), - ) - } + }, [onPressPublish, publishOnUpload, videoUploadState.pendingPublish]) const canPost = useMemo( () => graphemeLength <= MAX_GRAPHEME_LENGTH && !isAltTextRequiredAndMissing, @@ -1058,18 +1099,23 @@ function VideoUploadToolbar({state}: {state: VideoUploadState}) { } // we could use state.jobStatus?.progress but 99% of the time it jumps from 0 to 100 - const progress = + let progress = state.status === 'compressing' || state.status === 'uploading' ? state.progress : 100 + if (state.error) { + text = _('Error') + progress = 100 + } + return ( {text} From 39f74ced5c81bb103a87cd39b0f1dae955dbb31d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 3 Sep 2024 23:13:25 +0100 Subject: [PATCH 02/76] close keyboard before opening modal (#5124) --- src/view/com/composer/videos/SubtitleDialog.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/view/com/composer/videos/SubtitleDialog.tsx b/src/view/com/composer/videos/SubtitleDialog.tsx index 90a29b25..9cd8eae4 100644 --- a/src/view/com/composer/videos/SubtitleDialog.tsx +++ b/src/view/com/composer/videos/SubtitleDialog.tsx @@ -1,5 +1,5 @@ import React, {useCallback} from 'react' -import {StyleProp, View, ViewStyle} from 'react-native' +import {Keyboard, StyleProp, View, ViewStyle} from 'react-native' import RNPickerSelect from 'react-native-picker-select' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -45,7 +45,10 @@ export function SubtitleDialogBtn(props: Props) { size="xsmall" color="secondary" variant="ghost" - onPress={control.open}> + onPress={() => { + if (Keyboard.isVisible()) Keyboard.dismiss() + control.open() + }}> {isWeb ? Captions & alt text : Alt text} From 8860890a8588bc3768a5146abee9510127cc70ed Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 4 Sep 2024 14:41:42 +0100 Subject: [PATCH 03/76] 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 81707d2b..7d86f407 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 04/76] 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 d81f250b..eaa760b4 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 05/76] 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 ee072449..0c65e226 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 06/76] 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 e802addd..4c44781c 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 07/76] [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 55ac1882..03838b66 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 08/76] [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 77616d78..bdc7967c 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 03838b66..988ba573 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 15/76] 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 d8aa598e..a6eadd25 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 16/76] 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 a6eadd25..6a6ac726 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 17/76] [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 d8410951..a5bc97f8 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 4fafce1d..3fa15926 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) { From d846f5bbf0a0548181331dfd095b70d36afd9ad9 Mon Sep 17 00:00:00 2001 From: gabrielsiilva Date: Thu, 5 Sep 2024 01:00:07 -0300 Subject: [PATCH 24/76] fix on 'reposted by you' translation to ptbr (#5146) --- src/locale/locales/pt-BR/messages.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locale/locales/pt-BR/messages.po b/src/locale/locales/pt-BR/messages.po index 98560bf9..3a67499b 100644 --- a/src/locale/locales/pt-BR/messages.po +++ b/src/locale/locales/pt-BR/messages.po @@ -5576,7 +5576,7 @@ msgstr "Repostado por <0><1/>" #: src/view/com/posts/FeedItem.tsx:292 #: src/view/com/posts/FeedItem.tsx:311 msgid "Reposted by you" -msgstr "repostou para você" +msgstr "Repostado por você" #: src/view/com/notifications/FeedItem.tsx:184 msgid "reposted your post" From 60b74f7ab82328de5ec9cea7c40e1db705d40d6b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 5 Sep 2024 15:56:10 +0100 Subject: [PATCH 25/76] [Video] Disable autoplay option (preview + web player) (#5167) * rename setting * preview (web) * preview (native) * improve autoplay disabled behaviour on web --- src/view/com/composer/videos/VideoPreview.tsx | 12 +++++++++++- src/view/com/composer/videos/VideoPreview.web.tsx | 11 ++++++++++- .../VideoEmbedInner/VideoWebControls.tsx | 15 ++++++++++----- src/view/screens/AccessibilitySettings.tsx | 2 +- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/view/com/composer/videos/VideoPreview.tsx b/src/view/com/composer/videos/VideoPreview.tsx index 199a1fff..28b46bae 100644 --- a/src/view/com/composer/videos/VideoPreview.tsx +++ b/src/view/com/composer/videos/VideoPreview.tsx @@ -6,8 +6,10 @@ import {useVideoPlayer, VideoView} from 'expo-video' import {CompressedVideo} from '#/lib/media/video/types' import {clamp} from '#/lib/numbers' +import {useAutoplayDisabled} from '#/state/preferences' import {ExternalEmbedRemoveBtn} from 'view/com/composer/ExternalEmbedRemoveBtn' import {atoms as a, useTheme} from '#/alf' +import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' export function VideoPreview({ asset, @@ -20,10 +22,13 @@ export function VideoPreview({ clear: () => void }) { const t = useTheme() + const autoplayDisabled = useAutoplayDisabled() const player = useVideoPlayer(video.uri, player => { player.loop = true player.muted = true - player.play() + if (!autoplayDisabled) { + player.play() + } }) let aspectRatio = asset.width / asset.height @@ -53,6 +58,11 @@ export function VideoPreview({ contentFit="contain" /> + {autoplayDisabled && ( + + + + )} ) } diff --git a/src/view/com/composer/videos/VideoPreview.web.tsx b/src/view/com/composer/videos/VideoPreview.web.tsx index 4c44781c..9473be07 100644 --- a/src/view/com/composer/videos/VideoPreview.web.tsx +++ b/src/view/com/composer/videos/VideoPreview.web.tsx @@ -6,9 +6,11 @@ import {useLingui} from '@lingui/react' import {CompressedVideo} from '#/lib/media/video/types' import {clamp} from '#/lib/numbers' +import {useAutoplayDisabled} from '#/state/preferences' import * as Toast from '#/view/com/util/Toast' import {ExternalEmbedRemoveBtn} from 'view/com/composer/ExternalEmbedRemoveBtn' import {atoms as a} from '#/alf' +import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' export function VideoPreview({ asset, @@ -23,6 +25,7 @@ export function VideoPreview({ }) { const ref = useRef(null) const {_} = useLingui() + const autoplayDisabled = useAutoplayDisabled() useEffect(() => { if (!ref.current) return @@ -66,17 +69,23 @@ export function VideoPreview({ {aspectRatio}, a.overflow_hidden, {backgroundColor: 'black'}, + a.relative, ]}>