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 {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>
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user