diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 1cc646b403..b464a88fca 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -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 = ({
}
setError('')}
- clearVideo={clearVideo}
+ clearVideo={
+ erroredVideoPostId
+ ? () => clearVideo(erroredVideoPostId)
+ : () => {}
+ }
/>
@@ -485,35 +547,36 @@ export const ComposePost = ({
onLayout={onScrollViewLayout}>
{replyTo ? : undefined}
selectVideo(activePost.id, asset)}
+ onClearVideo={() => clearVideo(activePost.id)}
onPublish={() => onPressPublish(false)}
onError={setError}
/>
-
-
-
-
-
+
+
+
+ selectVideo(activePost.id, asset)}
+ />
+
void
+ post: PostDraft
+ dispatch: (action: ComposerAction) => void
textInput: React.Ref
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({
>
@@ -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({
diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts
index 353600ef60..4c94d5eac9 100644
--- a/src/view/com/composer/state/composer.ts
+++ b/src/view/com/composer/state/composer.ts
@@ -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: [],
diff --git a/src/view/com/composer/state/video.ts b/src/view/com/composer/state/video.ts
index e29687200f..8814a7e61c 100644
--- a/src/view/com/composer/state/video.ts
+++ b/src/view/com/composer/state/video.ts
@@ -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,