diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 249ba99e51..a523c3f523 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -42,6 +42,7 @@ import {
AppBskyFeedDefs,
AppBskyFeedGetPostThread,
BskyAgent,
+ RichText,
} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
@@ -112,8 +113,11 @@ import * as Prompt from '#/components/Prompt'
import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
import {
+ ComposerAction,
+ ComposerDraft,
composerReducer,
createComposerState,
+ EmbedDraft,
MAX_IMAGES,
} from './state/composer'
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
@@ -142,10 +146,7 @@ export const ComposePost = ({
const agent = useAgent()
const queryClient = useQueryClient()
const currentDid = currentAccount!.did
- const {data: currentProfile} = useProfileQuery({did: currentDid})
const {closeComposer} = useComposerControls()
- const pal = usePalette('default')
- const {isMobile} = useWebMediaQueries()
const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled()
const langPrefs = useLanguagePrefs()
@@ -154,11 +155,10 @@ export const ComposePost = ({
const discardPromptControl = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext()
const {closeAllModals} = useModalControls()
- const t = useTheme()
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
- const [isProcessing, setIsProcessing] = useState(false)
- const [processingState, setProcessingState] = useState('')
+ const [isPublishing, setIsPublishing] = useState(false)
+ const [publishingStage, setPublishingStage] = useState('')
const [error, setError] = useState('')
const [draft, dispatch] = useReducer(
@@ -222,22 +222,6 @@ export const ComposePost = ({
dispatch({type: 'embed_remove_video'})
}, [videoState.abortController, dispatch])
- const updateVideoDimensions = useCallback(
- (width: number, height: number) => {
- dispatch({
- type: 'embed_update_video',
- videoAction: {
- type: 'update_dimensions',
- width,
- height,
- signal: videoState.abortController.signal,
- },
- })
- },
- [videoState.abortController],
- )
-
- const hasVideo = Boolean(videoState.asset || videoState.video)
const [publishOnUpload, setPublishOnUpload] = useState(false)
const onClose = useCallback(() => {
@@ -299,32 +283,6 @@ export const ComposePost = ({
}
}, [onPressCancel, closeAllDialogs, closeAllModals])
- const onNewLink = useCallback((uri: string) => {
- dispatch({type: 'embed_add_uri', uri})
- }, [])
-
- const onImageAdd = useCallback(
- (next: ComposerImage[]) => {
- dispatch({
- type: 'embed_add_images',
- images: next,
- })
- },
- [dispatch],
- )
-
- const onPhotoPasted = useCallback(
- async (uri: string) => {
- if (uri.startsWith('data:video/')) {
- selectVideo({uri, type: 'video', height: 0, width: 0})
- } else {
- const res = await pasteImage(uri)
- onImageAdd([res])
- }
- },
- [selectVideo, onImageAdd],
- )
-
const isAltTextRequiredAndMissing = useMemo(() => {
if (!requireAltTextEnabled) return false
@@ -336,8 +294,8 @@ export const ComposePost = ({
}, [images, extGifAlt, extGif, requireAltTextEnabled])
const onPressPublish = React.useCallback(
- async (finishedUploading?: boolean) => {
- if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) {
+ async (finishedUploading: boolean) => {
+ if (isPublishing || graphemeLength > MAX_GRAPHEME_LENGTH) {
return
}
@@ -368,7 +326,7 @@ export const ComposePost = ({
return
}
- setIsProcessing(true)
+ setIsPublishing(true)
let postUri
try {
@@ -376,7 +334,7 @@ export const ComposePost = ({
await apilib.post(agent, queryClient, {
draft: draft,
replyTo: replyTo?.uri,
- onStateChange: setProcessingState,
+ onStateChange: setPublishingStage,
langs: toPostLanguages(langPrefs.postLanguage),
})
).uri
@@ -406,7 +364,7 @@ export const ComposePost = ({
err = _(msg`This post's author has disabled quote posts.`)
}
setError(err)
- setIsProcessing(false)
+ setIsPublishing(false)
return
} finally {
if (postUri) {
@@ -456,7 +414,7 @@ export const ComposePost = ({
images,
graphemeLength,
isAltTextRequiredAndMissing,
- isProcessing,
+ isPublishing,
langPrefs.postLanguage,
onClose,
onPost,
@@ -484,28 +442,11 @@ export const ComposePost = ({
() => graphemeLength <= MAX_GRAPHEME_LENGTH && !isAltTextRequiredAndMissing,
[graphemeLength, isAltTextRequiredAndMissing],
)
- const selectTextInputPlaceholder = replyTo
- ? _(msg`Write your reply`)
- : _(msg`What's up?`)
-
- const canSelectImages =
- images.length < MAX_IMAGES &&
- videoState.status === 'idle' &&
- !videoState.video
- const hasMedia = images.length > 0 || Boolean(videoState.video)
const onEmojiButtonPress = useCallback(() => {
openEmojiPicker?.(textInput.current?.getCursorPosition())
}, [openEmojiPicker])
- const onSelectGif = useCallback((gif: Gif) => {
- dispatch({type: 'embed_add_gif', gif})
- }, [])
-
- const handleChangeGifAltText = useCallback((altText: string) => {
- dispatch({type: 'embed_update_gif', alt: altText})
- }, [])
-
const {
scrollHandler,
onScrollViewContentSizeChange,
@@ -527,86 +468,24 @@ export const ComposePost = ({
style={[a.flex_1, viewStyles]}
aria-modal
accessibilityViewIsModal>
-
-
-
-
- {isProcessing ? (
- <>
- {processingState}
-
-
-
- >
- ) : canPost ? (
-
- ) : (
-
-
- Post
-
-
- )}
-
-
- {isAltTextRequiredAndMissing && (
-
-
-
-
-
- One or more images is missing alt text.
-
-
- )}
+ onPressPublish(false)}>
+ {isAltTextRequiredAndMissing && }
setError('')}
clearVideo={clearVideo}
/>
-
+
+
{replyTo ? : undefined}
-
-
-
- {
- dispatch({type: 'update_richtext', richtext: rt})
- }}
- onPhotoPasted={onPhotoPasted}
- onPressPublish={() => onPressPublish()}
- onNewLink={onNewLink}
- onError={setError}
- accessible={true}
- accessibilityLabel={_(msg`Write post`)}
- accessibilityHint={_(
- msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
- )}
- />
-
-
-
-
- {extGif && (
-
- {
- dispatch({type: 'embed_remove_gif'})
- }}
- />
-
-
- )}
-
- {!draft.embed.media && extLink && (
-
- {
- dispatch({type: 'embed_remove_link'})
- }}
- />
-
- )}
-
-
- {hasVideo && (
-
- {videoState.asset &&
- (videoState.status === 'compressing' ? (
-
- ) : videoState.video ? (
-
- ) : null)}
-
- dispatch({
- type: 'embed_update_video',
- videoAction: {
- type: 'update_alt_text',
- altText,
- signal: videoState.abortController.signal,
- },
- })
- }
- captions={videoState.captions}
- setCaptions={updater => {
- dispatch({
- type: 'embed_update_video',
- videoAction: {
- type: 'update_captions',
- updater,
- signal: videoState.abortController.signal,
- },
- })
- }}
- />
-
- )}
-
-
- {quote ? (
-
-
-
-
- {!initQuote && (
- {
- dispatch({type: 'embed_remove_quote'})
- }}
- />
- )}
-
- ) : null}
-
+ onPressPublish(false)}
+ onError={setError}
+ />
+
-
-
- {replyTo ? null : (
- {
- dispatch({type: 'update_postgate', postgate: nextPostgate})
- }}
- threadgateAllowUISettings={draft.threadgate}
- onChangeThreadgateAllowUISettings={nextThreadgate => {
- dispatch({
- type: 'update_threadgate',
- threadgate: nextThreadgate,
- })
- }}
- style={bottomBarAnimatedStyle}
- />
- )}
- {
- dispatch({type: 'update_labels', labels: nextLabels})
- }}
- hasMedia={hasMedia || Boolean(extLink)}
- />
-
-
-
-
- {videoState.status !== 'idle' && videoState.status !== 'done' ? (
-
- ) : (
-
-
- 0}
- setError={setError}
- />
-
-
- {!isMobile ? (
-
- ) : null}
-
- )}
-
-
-
-
-
-
+
+
+
+
void
+ textInput: React.Ref
+ isReply: boolean
+ canRemoveQuote: boolean
+ onClearVideo: () => void
+ onSelectVideo: (asset: ImagePickerAsset) => void
+ onError: (error: string) => void
+ onPublish: (richtext: RichText) => void
+}) {
+ const {currentAccount} = useSession()
+ const currentDid = currentAccount!.did
+ const {_} = useLingui()
+ const {data: currentProfile} = useProfileQuery({did: currentDid})
+ const richtext = draft.richtext
+ const selectTextInputPlaceholder = isReply
+ ? _(msg`Write your reply`)
+ : _(msg`What's up?`)
+
+ const onImageAdd = useCallback(
+ (next: ComposerImage[]) => {
+ dispatch({
+ type: 'embed_add_images',
+ images: next,
+ })
+ },
+ [dispatch],
+ )
+
+ const onNewLink = useCallback(
+ (uri: string) => {
+ dispatch({type: 'embed_add_uri', uri})
+ },
+ [dispatch],
+ )
+
+ const onPhotoPasted = useCallback(
+ async (uri: string) => {
+ if (uri.startsWith('data:video/')) {
+ onSelectVideo({uri, type: 'video', height: 0, width: 0})
+ } else {
+ const res = await pasteImage(uri)
+ onImageAdd([res])
+ }
+ },
+ [onSelectVideo, onImageAdd],
+ )
+
+ return (
+ <>
+
+
+ {
+ dispatch({type: 'update_richtext', richtext: rt})
+ }}
+ onPhotoPasted={onPhotoPasted}
+ onNewLink={onNewLink}
+ onError={onError}
+ onPressPublish={onPublish}
+ accessible={true}
+ accessibilityLabel={_(msg`Write post`)}
+ accessibilityHint={_(
+ msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
+ )}
+ />
+
+
+
+ >
+ )
+}
+
+function ComposerTopBar({
+ canPost,
+ isReply,
+ isPublishQueued,
+ isPublishing,
+ publishingStage,
+ onCancel,
+ onPublish,
+ topBarAnimatedStyle,
+ children,
+}: {
+ isPublishing: boolean
+ publishingStage: string
+ canPost: boolean
+ isReply: boolean
+ isPublishQueued: boolean
+ onCancel: () => void
+ onPublish: () => void
+ topBarAnimatedStyle: StyleProp
+ children?: React.ReactNode
+}) {
+ const pal = usePalette('default')
+ return (
+
+
+
+
+ {isPublishing ? (
+ <>
+ {publishingStage}
+
+
+
+ >
+ ) : canPost ? (
+
+ ) : (
+
+
+ Post
+
+
+ )}
+
+ {children}
+
+ )
+}
+
+function AltTextReminder() {
+ const pal = usePalette('default')
+ return (
+
+
+
+
+
+ One or more images is missing alt text.
+
+
+ )
+}
+
+function ComposerEmbeds({
+ embed,
+ dispatch,
+ clearVideo,
+ canRemoveQuote,
+}: {
+ embed: EmbedDraft
+ dispatch: (action: ComposerAction) => void
+ clearVideo: () => void
+ canRemoveQuote: boolean
+}) {
+ const video = embed.media?.type === 'video' ? embed.media.video : null
+ return (
+ <>
+ {embed.media?.type === 'images' && (
+
+ )}
+
+ {embed.media?.type === 'gif' && (
+
+ dispatch({type: 'embed_remove_gif'})}
+ />
+ {
+ dispatch({type: 'embed_update_gif', alt: altText})
+ }}
+ />
+
+ )}
+
+ {!embed.media && embed.link && (
+
+ dispatch({type: 'embed_remove_link'})}
+ />
+
+ )}
+
+
+ {video && (
+
+ {video.asset &&
+ (video.status === 'compressing' ? (
+
+ ) : video.video ? (
+ {
+ dispatch({
+ type: 'embed_update_video',
+ videoAction: {
+ type: 'update_dimensions',
+ width,
+ height,
+ signal: video.abortController.signal,
+ },
+ })
+ }}
+ clear={clearVideo}
+ />
+ ) : null)}
+
+ dispatch({
+ type: 'embed_update_video',
+ videoAction: {
+ type: 'update_alt_text',
+ altText,
+ signal: video.abortController.signal,
+ },
+ })
+ }
+ captions={video.captions}
+ setCaptions={updater => {
+ dispatch({
+ type: 'embed_update_video',
+ videoAction: {
+ type: 'update_captions',
+ updater,
+ signal: video.abortController.signal,
+ },
+ })
+ }}
+ />
+
+ )}
+
+
+
+ {embed.quote?.uri ? (
+
+
+
+
+ {canRemoveQuote && (
+ dispatch({type: 'embed_remove_quote'})} />
+ )}
+
+ ) : null}
+
+ >
+ )
+}
+
+function ComposerPills({
+ isReply,
+ draft,
+ dispatch,
+ bottomBarAnimatedStyle,
+}: {
+ isReply: boolean
+ draft: ComposerDraft
+ dispatch: (action: ComposerAction) => void
+ bottomBarAnimatedStyle: StyleProp
+}) {
+ const t = useTheme()
+ const media = draft.embed.media
+ const hasMedia = media?.type === 'images' || media?.type === 'video'
+ const hasLink = !!draft.embed.link
+ return (
+
+
+ {isReply ? null : (
+ {
+ dispatch({type: 'update_postgate', postgate: nextPostgate})
+ }}
+ threadgateAllowUISettings={draft.threadgate}
+ onChangeThreadgateAllowUISettings={nextThreadgate => {
+ dispatch({
+ type: 'update_threadgate',
+ threadgate: nextThreadgate,
+ })
+ }}
+ style={bottomBarAnimatedStyle}
+ />
+ )}
+ {
+ dispatch({type: 'update_labels', labels: nextLabels})
+ }}
+ hasMedia={hasMedia || hasLink}
+ />
+
+
+ )
+}
+
+function ComposerFooter({
+ draft,
+ dispatch,
+ graphemeLength,
+ onEmojiButtonPress,
+ onError,
+ onSelectVideo,
+}: {
+ draft: ComposerDraft
+ dispatch: (action: ComposerAction) => void
+ graphemeLength: number
+ onEmojiButtonPress: () => void
+ onError: (error: string) => void
+ onSelectVideo: (asset: ImagePickerAsset) => void
+}) {
+ const t = useTheme()
+ const {_} = useLingui()
+ const {isMobile} = useWebMediaQueries()
+
+ const media = draft.embed.media
+ const images = media?.type === 'images' ? media.images : []
+ const video = media?.type === 'video' ? media.video : null
+ const isMaxImages = images.length >= MAX_IMAGES
+
+ const onImageAdd = useCallback(
+ (next: ComposerImage[]) => {
+ dispatch({
+ type: 'embed_add_images',
+ images: next,
+ })
+ },
+ [dispatch],
+ )
+
+ const onSelectGif = useCallback(
+ (gif: Gif) => {
+ dispatch({type: 'embed_add_gif', gif})
+ },
+ [dispatch],
+ )
+
+ return (
+
+
+ {video && video.status !== 'done' ? (
+
+ ) : (
+
+
+
+
+
+ {!isMobile ? (
+
+ ) : null}
+
+ )}
+
+
+
+
+
+
+ )
+}
+
export function useComposerCancelRef() {
return useRef(null)
}
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx
index 43074fa5bd..11bbf13d2a 100644
--- a/src/view/com/composer/text-input/TextInput.tsx
+++ b/src/view/com/composer/text-input/TextInput.tsx
@@ -45,7 +45,7 @@ interface TextInputProps extends ComponentProps {
placeholder: string
setRichText: (v: RichText) => void
onPhotoPasted: (uri: string) => void
- onPressPublish: (richtext: RichText) => Promise
+ onPressPublish: (richtext: RichText) => void
onNewLink: (uri: string) => void
onError: (err: string) => void
}
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index acec61516a..1d7908e167 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -13,15 +13,15 @@ import {Text as TiptapText} from '@tiptap/extension-text'
import {generateJSON} from '@tiptap/html'
import {EditorContent, JSONContent, useEditor} from '@tiptap/react'
+import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {usePalette} from '#/lib/hooks/usePalette'
+import {blobToDataUri, isUriImage} from '#/lib/media/util'
import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
-import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
-import {blobToDataUri, isUriImage} from 'lib/media/util'
-import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
import {
LinkFacetMatch,
suggestLinkCardUri,
-} from 'view/com/composer/text-input/text-input-util'
+} from '#/view/com/composer/text-input/text-input-util'
+import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
import {atoms as a, useAlf} from '#/alf'
import {Portal} from '#/components/Portal'
import {normalizeTextStyles} from '#/components/Typography'
@@ -43,7 +43,7 @@ interface TextInputProps {
suggestedLinks: Set
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
onPhotoPasted: (uri: string) => void
- onPressPublish: (richtext: RichText) => Promise
+ onPressPublish: (richtext: RichText) => void
onNewLink: (uri: string) => void
onError: (err: string) => void
}