Check video state for the entire thread (#5957)

* Switch to using post ID for post actions

* Pass post-bound dispatch to ComposerPost

* Check video state for entire thread

* Always bind post actions to an ID

* Rename variable for consistency

* Fix clashing keys
This commit is contained in:
dan
2024-11-01 03:37:30 +00:00
committed by GitHub
parent 68bb451051
commit 4c31403330
3 changed files with 162 additions and 76 deletions
+143 -69
View File
@@ -55,6 +55,7 @@ import {until} from '#/lib/async/until'
import {MAX_GRAPHEME_LENGTH} from '#/lib/constants' import {MAX_GRAPHEME_LENGTH} from '#/lib/constants'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
@@ -168,46 +169,78 @@ export const ComposePost = ({
// TODO: Display drafts for other posts in the thread. // TODO: Display drafts for other posts in the thread.
const thread = composerState.thread const thread = composerState.thread
const draft = thread.posts[composerState.activePostIndex] const activePost = thread.posts[composerState.activePostIndex]
const dispatch = useCallback((postAction: PostAction) => { const dispatch = useCallback(
composerDispatch({ (postAction: PostAction) => {
type: 'update_post', composerDispatch({
postAction, type: 'update_post',
}) postId: activePost.id,
}, []) postAction,
})
let videoState: VideoState | NoVideoState = NO_VIDEO },
if (draft.embed.media?.type === 'video') { [activePost.id],
videoState = draft.embed.media.video )
}
const selectVideo = React.useCallback( const selectVideo = React.useCallback(
(asset: ImagePickerAsset) => { (postId: string, asset: ImagePickerAsset) => {
const abortController = new AbortController() const abortController = new AbortController()
dispatch({type: 'embed_add_video', asset, abortController}) composerDispatch({
type: 'update_post',
postId: postId,
postAction: {
type: 'embed_add_video',
asset,
abortController,
},
})
processVideo( processVideo(
asset, asset,
videoAction => dispatch({type: 'embed_update_video', videoAction}), videoAction => {
composerDispatch({
type: 'update_post',
postId: postId,
postAction: {
type: 'embed_update_video',
videoAction,
},
})
},
agent, agent,
currentDid, currentDid,
abortController.signal, abortController.signal,
_, _,
) )
}, },
[_, agent, currentDid, dispatch], [_, agent, currentDid, composerDispatch],
) )
// Whenever we receive an initial video uri, we should immediately run compression if necessary const onInitVideo = useNonReactiveCallback(() => {
useEffect(() => {
if (initVideoUri) { if (initVideoUri) {
selectVideo(initVideoUri) selectVideo(activePost.id, initVideoUri)
} }
}, [initVideoUri, selectVideo]) })
const clearVideo = React.useCallback(() => { useEffect(() => {
videoState.abortController.abort() onInitVideo()
dispatch({type: 'embed_remove_video'}) }, [onInitVideo])
}, [videoState.abortController, dispatch])
const clearVideo = React.useCallback(
(postId: string) => {
const post = thread.posts.find(p => p.id === postId)
const postMedia = post?.embed.media
if (postMedia?.type === 'video') {
postMedia.video.abortController.abort()
composerDispatch({
type: 'update_post',
postId: postId,
postAction: {
type: 'embed_remove_video',
},
})
}
},
[thread, composerDispatch],
)
const [publishOnUpload, setPublishOnUpload] = useState(false) const [publishOnUpload, setPublishOnUpload] = useState(false)
@@ -425,13 +458,38 @@ export const ComposePost = ({
) )
React.useEffect(() => { React.useEffect(() => {
if (videoState.pendingPublish && publishOnUpload) { if (publishOnUpload) {
if (!videoState.pendingPublish.mutableProcessed) { let uploadingVideos = 0
videoState.pendingPublish.mutableProcessed = true for (let post of thread.posts) {
if (post.embed.media?.type === 'video') {
const video = post.embed.media.video
if (!video.pendingPublish) {
uploadingVideos++
}
}
}
if (uploadingVideos === 0) {
setPublishOnUpload(false)
onPressPublish(true) onPressPublish(true)
} }
} }
}, [onPressPublish, publishOnUpload, videoState.pendingPublish]) }, [thread.posts, onPressPublish, publishOnUpload])
// TODO: It might make more sense to display this error per-post.
// Right now we're just displaying the first one.
let erroredVideoPostId: string | undefined
let erroredVideo: VideoState | NoVideoState = NO_VIDEO
for (let i = 0; i < thread.posts.length; i++) {
const post = thread.posts[i]
if (
post.embed.media?.type === 'video' &&
post.embed.media.video.status === 'error'
) {
erroredVideoPostId = post.id
erroredVideo = post.embed.media.video
break
}
}
const onEmojiButtonPress = useCallback(() => { const onEmojiButtonPress = useCallback(() => {
openEmojiPicker?.(textInput.current?.getCursorPosition()) openEmojiPicker?.(textInput.current?.getCursorPosition())
@@ -461,7 +519,7 @@ export const ComposePost = ({
<ComposerTopBar <ComposerTopBar
canPost={canPost} canPost={canPost}
isReply={!!replyTo} isReply={!!replyTo}
isPublishQueued={videoState.status !== 'idle' && publishOnUpload} isPublishQueued={publishOnUpload}
isPublishing={isPublishing} isPublishing={isPublishing}
publishingStage={publishingStage} publishingStage={publishingStage}
topBarAnimatedStyle={topBarAnimatedStyle} topBarAnimatedStyle={topBarAnimatedStyle}
@@ -470,9 +528,13 @@ export const ComposePost = ({
{isAltTextRequiredAndMissing && <AltTextReminder />} {isAltTextRequiredAndMissing && <AltTextReminder />}
<ErrorBanner <ErrorBanner
error={error} error={error}
videoState={videoState} videoState={erroredVideo}
clearError={() => setError('')} clearError={() => setError('')}
clearVideo={clearVideo} clearVideo={
erroredVideoPostId
? () => clearVideo(erroredVideoPostId)
: () => {}
}
/> />
</ComposerTopBar> </ComposerTopBar>
@@ -485,35 +547,36 @@ export const ComposePost = ({
onLayout={onScrollViewLayout}> onLayout={onScrollViewLayout}>
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined} {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
<ComposerPost <ComposerPost
draft={draft} key={activePost.id}
dispatch={dispatch} post={activePost}
dispatch={composerDispatch}
textInput={textInput} textInput={textInput}
isReply={!!replyTo} isReply={!!replyTo}
canRemoveQuote={!initQuote} canRemoveQuote={!initQuote}
onSelectVideo={selectVideo} onSelectVideo={asset => selectVideo(activePost.id, asset)}
onClearVideo={clearVideo} onClearVideo={() => clearVideo(activePost.id)}
onPublish={() => onPressPublish(false)} onPublish={() => onPressPublish(false)}
onError={setError} onError={setError}
/> />
</Animated.ScrollView> </Animated.ScrollView>
<SuggestedLanguage text={draft.richtext.text} /> <React.Fragment key={activePost.id}>
<SuggestedLanguage text={activePost.richtext.text} />
<ComposerPills <ComposerPills
isReply={!!replyTo} isReply={!!replyTo}
post={draft} post={activePost}
thread={composerState.thread} thread={composerState.thread}
dispatch={composerDispatch} dispatch={composerDispatch}
bottomBarAnimatedStyle={bottomBarAnimatedStyle} bottomBarAnimatedStyle={bottomBarAnimatedStyle}
/> />
<ComposerFooter
<ComposerFooter post={activePost}
draft={draft} dispatch={dispatch}
dispatch={dispatch} onError={setError}
onError={setError} onEmojiButtonPress={onEmojiButtonPress}
onEmojiButtonPress={onEmojiButtonPress} onSelectVideo={asset => selectVideo(activePost.id, asset)}
onSelectVideo={selectVideo} />
/> </React.Fragment>
</View> </View>
<Prompt.Basic <Prompt.Basic
@@ -530,7 +593,7 @@ export const ComposePost = ({
} }
function ComposerPost({ function ComposerPost({
draft, post,
dispatch, dispatch,
textInput, textInput,
isReply, isReply,
@@ -540,8 +603,8 @@ function ComposerPost({
onError, onError,
onPublish, onPublish,
}: { }: {
draft: PostDraft post: PostDraft
dispatch: (action: PostAction) => void dispatch: (action: ComposerAction) => void
textInput: React.Ref<TextInputRef> textInput: React.Ref<TextInputRef>
isReply: boolean isReply: boolean
canRemoveQuote: boolean canRemoveQuote: boolean
@@ -554,29 +617,39 @@ function ComposerPost({
const currentDid = currentAccount!.did const currentDid = currentAccount!.did
const {_} = useLingui() const {_} = useLingui()
const {data: currentProfile} = useProfileQuery({did: currentDid}) const {data: currentProfile} = useProfileQuery({did: currentDid})
const richtext = draft.richtext const richtext = post.richtext
const isTextOnly = const isTextOnly = !post.embed.link && !post.embed.quote && !post.embed.media
!draft.embed.link && !draft.embed.quote && !draft.embed.media
const forceMinHeight = isWeb && isTextOnly const forceMinHeight = isWeb && isTextOnly
const selectTextInputPlaceholder = isReply const selectTextInputPlaceholder = isReply
? _(msg`Write your reply`) ? _(msg`Write your reply`)
: _(msg`What's up?`) : _(msg`What's up?`)
const dispatchPost = useCallback(
(action: PostAction) => {
dispatch({
type: 'update_post',
postId: post.id,
postAction: action,
})
},
[dispatch, post.id],
)
const onImageAdd = useCallback( const onImageAdd = useCallback(
(next: ComposerImage[]) => { (next: ComposerImage[]) => {
dispatch({ dispatchPost({
type: 'embed_add_images', type: 'embed_add_images',
images: next, images: next,
}) })
}, },
[dispatch], [dispatchPost],
) )
const onNewLink = useCallback( const onNewLink = useCallback(
(uri: string) => { (uri: string) => {
dispatch({type: 'embed_add_uri', uri}) dispatchPost({type: 'embed_add_uri', uri})
}, },
[dispatch], [dispatchPost],
) )
const onPhotoPasted = useCallback( const onPhotoPasted = useCallback(
@@ -610,7 +683,7 @@ function ComposerPost({
autoFocus autoFocus
webForceMinHeight={forceMinHeight} webForceMinHeight={forceMinHeight}
setRichText={rt => { setRichText={rt => {
dispatch({type: 'update_richtext', richtext: rt}) dispatchPost({type: 'update_richtext', richtext: rt})
}} }}
onPhotoPasted={onPhotoPasted} onPhotoPasted={onPhotoPasted}
onNewLink={onNewLink} onNewLink={onNewLink}
@@ -626,8 +699,8 @@ function ComposerPost({
<ComposerEmbeds <ComposerEmbeds
canRemoveQuote={canRemoveQuote} canRemoveQuote={canRemoveQuote}
embed={draft.embed} embed={post.embed}
dispatch={dispatch} dispatch={dispatchPost}
clearVideo={onClearVideo} clearVideo={onClearVideo}
/> />
</> </>
@@ -898,6 +971,7 @@ function ComposerPills({
onChange={nextLabels => { onChange={nextLabels => {
dispatch({ dispatch({
type: 'update_post', type: 'update_post',
postId: post.id,
postAction: { postAction: {
type: 'update_labels', type: 'update_labels',
labels: nextLabels, labels: nextLabels,
@@ -912,13 +986,13 @@ function ComposerPills({
} }
function ComposerFooter({ function ComposerFooter({
draft, post,
dispatch, dispatch,
onEmojiButtonPress, onEmojiButtonPress,
onError, onError,
onSelectVideo, onSelectVideo,
}: { }: {
draft: PostDraft post: PostDraft
dispatch: (action: PostAction) => void dispatch: (action: PostAction) => void
onEmojiButtonPress: () => void onEmojiButtonPress: () => void
onError: (error: string) => void onError: (error: string) => void
@@ -928,7 +1002,7 @@ function ComposerFooter({
const {_} = useLingui() const {_} = useLingui()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const media = draft.embed.media const media = post.embed.media
const images = media?.type === 'images' ? media.images : [] const images = media?.type === 'images' ? media.images : []
const video = media?.type === 'video' ? media.video : null const video = media?.type === 'video' ? media.video : null
const isMaxImages = images.length >= MAX_IMAGES const isMaxImages = images.length >= MAX_IMAGES
@@ -1000,7 +1074,7 @@ function ComposerFooter({
<View style={[a.flex_row, a.align_center, a.justify_between]}> <View style={[a.flex_row, a.align_center, a.justify_between]}>
<SelectLangBtn /> <SelectLangBtn />
<CharProgress <CharProgress
count={draft.shortenedGraphemeLength} count={post.shortenedGraphemeLength}
style={{width: 65}} style={{width: 65}}
/> />
</View> </View>
+18 -5
View File
@@ -1,5 +1,6 @@
import {ImagePickerAsset} from 'expo-image-picker' import {ImagePickerAsset} from 'expo-image-picker'
import {AppBskyFeedPostgate, RichText} from '@atproto/api' import {AppBskyFeedPostgate, RichText} from '@atproto/api'
import {nanoid} from 'nanoid/non-secure'
import {SelfLabel} from '#/lib/moderation' import {SelfLabel} from '#/lib/moderation'
import {insertMentionAt} from '#/lib/strings/mention-manip' import {insertMentionAt} from '#/lib/strings/mention-manip'
@@ -49,6 +50,7 @@ export type EmbedDraft = {
} }
export type PostDraft = { export type PostDraft = {
id: string
richtext: RichText richtext: RichText
labels: SelfLabel[] labels: SelfLabel[]
embed: EmbedDraft embed: EmbedDraft
@@ -89,7 +91,11 @@ export type ComposerState = {
export type ComposerAction = export type ComposerAction =
| {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record}
| {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]}
| {type: 'update_post'; postAction: PostAction} | {
type: 'update_post'
postId: string
postAction: PostAction
}
export const MAX_IMAGES = 4 export const MAX_IMAGES = 4
@@ -117,11 +123,17 @@ export function composerReducer(
} }
} }
case 'update_post': { case 'update_post': {
const nextPosts = [...state.thread.posts] let nextPosts = state.thread.posts
nextPosts[state.activePostIndex] = postReducer( const postIndex = state.thread.posts.findIndex(
state.thread.posts[state.activePostIndex], p => p.id === action.postId,
action.postAction,
) )
if (postIndex !== -1) {
nextPosts = state.thread.posts.slice()
nextPosts[postIndex] = postReducer(
state.thread.posts[postIndex],
action.postAction,
)
}
return { return {
...state, ...state,
thread: { thread: {
@@ -427,6 +439,7 @@ export function createComposerState({
thread: { thread: {
posts: [ posts: [
{ {
id: nanoid(),
richtext: initRichText, richtext: initRichText,
shortenedGraphemeLength: 0, shortenedGraphemeLength: 0,
labels: [], labels: [],
+1 -2
View File
@@ -132,7 +132,7 @@ type DoneState = {
asset: ImagePickerAsset asset: ImagePickerAsset
video: CompressedVideo video: CompressedVideo
jobId?: undefined jobId?: undefined
pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean} pendingPublish: {blobRef: BlobRef}
altText: string altText: string
captions: CaptionsTrack[] captions: CaptionsTrack[]
} }
@@ -250,7 +250,6 @@ export function videoReducer(
video: state.video, video: state.video,
pendingPublish: { pendingPublish: {
blobRef: action.blobRef, blobRef: action.blobRef,
mutableProcessed: false,
}, },
altText: state.altText, altText: state.altText,
captions: state.captions, captions: state.captions,