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