about summary refs log tree commit diff
path: root/src/view/com/composer/Composer.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/composer/Composer.tsx')
-rw-r--r--src/view/com/composer/Composer.tsx568
1 files changed, 300 insertions, 268 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index f4e290ca8..e03c64a42 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -114,11 +114,14 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {createPortalGroup} from '#/components/Portal'
 import * as Prompt from '#/components/Prompt'
 import {Text as NewText} from '#/components/Typography'
 import {composerReducer, createComposerState} from './state/composer'
 import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
 
+const Portal = createPortalGroup()
+
 const MAX_IMAGES = 4
 
 type CancelRef = {
@@ -184,13 +187,10 @@ export const ComposePost = ({
     initQuote,
   )
 
-  const [videoAltText, setVideoAltText] = useState('')
-  const [captions, setCaptions] = useState<{lang: string; file: File}[]>([])
-
   // TODO: Move more state here.
   const [composerState, dispatch] = useReducer(
     composerReducer,
-    {initImageUris},
+    {initImageUris, initQuoteUri: initQuote?.uri},
     createComposerState,
   )
 
@@ -337,6 +337,7 @@ export const ComposePost = ({
 
   const onNewLink = useCallback(
     (uri: string) => {
+      dispatch({type: 'embed_add_uri', uri})
       if (extLink != null) return
       setExtLink({uri, isLoading: true})
     },
@@ -421,10 +422,9 @@ export const ComposePost = ({
       try {
         postUri = (
           await apilib.post(agent, {
-            composerState, // TODO: not used yet.
+            composerState, // TODO: move more state here.
             rawText: richtext.text,
             replyTo: replyTo?.uri,
-            images,
             quote,
             extLink,
             labels,
@@ -432,18 +432,6 @@ export const ComposePost = ({
             postgate,
             onStateChange: setProcessingState,
             langs: toPostLanguages(langPrefs.postLanguage),
-            video:
-              videoState.status === 'done'
-                ? {
-                    blobRef: videoState.pendingPublish.blobRef,
-                    altText: videoAltText,
-                    captions: captions,
-                    aspectRatio: {
-                      width: videoState.asset.width,
-                      height: videoState.asset.height,
-                    },
-                  }
-                : undefined,
           })
         ).uri
         try {
@@ -521,7 +509,6 @@ export const ComposePost = ({
     [
       _,
       agent,
-      captions,
       composerState,
       extLink,
       images,
@@ -540,9 +527,7 @@ export const ComposePost = ({
       setExtLink,
       setLangPrefs,
       threadgateAllowUISettings,
-      videoAltText,
       videoState.asset,
-      videoState.pendingPublish,
       videoState.status,
     ],
   )
@@ -582,6 +567,7 @@ export const ComposePost = ({
 
   const onSelectGif = useCallback(
     (gif: Gif) => {
+      dispatch({type: 'embed_add_gif', gif})
       setExtLink({
         uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`,
         isLoading: true,
@@ -600,6 +586,7 @@ export const ComposePost = ({
 
   const handleChangeGifAltText = useCallback(
     (altText: string) => {
+      dispatch({type: 'embed_update_gif', alt: altText})
       setExtLink(ext =>
         ext && ext.meta
           ? {
@@ -629,268 +616,313 @@ export const ComposePost = ({
   const keyboardVerticalOffset = useKeyboardVerticalOffset()
 
   return (
-    <KeyboardAvoidingView
-      testID="composePostView"
-      behavior={isIOS ? 'padding' : 'height'}
-      keyboardVerticalOffset={keyboardVerticalOffset}
-      style={a.flex_1}>
-      <View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
-        <Animated.View
-          style={topBarAnimatedStyle}
-          layout={native(LinearTransition)}>
-          <View style={styles.topbarInner}>
-            <Button
-              label={_(msg`Cancel`)}
-              variant="ghost"
-              color="primary"
-              shape="default"
-              size="small"
-              style={[
-                a.rounded_full,
-                a.py_sm,
-                {paddingLeft: 7, paddingRight: 7},
-              ]}
-              onPress={onPressCancel}
-              accessibilityHint={_(
-                msg`Closes post composer and discards post draft`,
-              )}>
-              <ButtonText style={[a.text_md]}>
-                <Trans>Cancel</Trans>
-              </ButtonText>
-            </Button>
-            <View style={a.flex_1} />
-            {isProcessing ? (
-              <>
-                <Text style={pal.textLight}>{processingState}</Text>
-                <View style={styles.postBtn}>
-                  <ActivityIndicator />
-                </View>
-              </>
-            ) : (
-              <View style={[styles.postBtnWrapper]}>
-                <LabelsBtn
-                  labels={labels}
-                  onChange={setLabels}
-                  hasMedia={hasMedia}
-                />
-                {canPost ? (
-                  <Button
-                    testID="composerPublishBtn"
-                    label={
-                      replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
-                    }
-                    variant="solid"
-                    color="primary"
-                    shape="default"
-                    size="small"
-                    style={[a.rounded_full, a.py_sm]}
-                    onPress={() => onPressPublish()}
-                    disabled={videoState.status !== 'idle' && publishOnUpload}>
-                    <ButtonText style={[a.text_md]}>
-                      {replyTo ? (
-                        <Trans context="action">Reply</Trans>
-                      ) : (
-                        <Trans context="action">Post</Trans>
-                      )}
-                    </ButtonText>
-                  </Button>
-                ) : (
-                  <View style={[styles.postBtn, pal.btn]}>
-                    <Text style={[pal.textLight, s.f16, s.bold]}>
-                      <Trans context="action">Post</Trans>
-                    </Text>
+    <Portal.Provider>
+      <KeyboardAvoidingView
+        testID="composePostView"
+        behavior={isIOS ? 'padding' : 'height'}
+        keyboardVerticalOffset={keyboardVerticalOffset}
+        style={a.flex_1}>
+        <View
+          style={[a.flex_1, viewStyles]}
+          aria-modal
+          accessibilityViewIsModal>
+          <Animated.View
+            style={topBarAnimatedStyle}
+            layout={native(LinearTransition)}>
+            <View style={styles.topbarInner}>
+              <Button
+                label={_(msg`Cancel`)}
+                variant="ghost"
+                color="primary"
+                shape="default"
+                size="small"
+                style={[
+                  a.rounded_full,
+                  a.py_sm,
+                  {paddingLeft: 7, paddingRight: 7},
+                ]}
+                onPress={onPressCancel}
+                accessibilityHint={_(
+                  msg`Closes post composer and discards post draft`,
+                )}>
+                <ButtonText style={[a.text_md]}>
+                  <Trans>Cancel</Trans>
+                </ButtonText>
+              </Button>
+              <View style={a.flex_1} />
+              {isProcessing ? (
+                <>
+                  <Text style={pal.textLight}>{processingState}</Text>
+                  <View style={styles.postBtn}>
+                    <ActivityIndicator />
                   </View>
-                )}
+                </>
+              ) : (
+                <View style={[styles.postBtnWrapper]}>
+                  <LabelsBtn
+                    labels={labels}
+                    onChange={setLabels}
+                    hasMedia={hasMedia}
+                  />
+                  {canPost ? (
+                    <Button
+                      testID="composerPublishBtn"
+                      label={
+                        replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
+                      }
+                      variant="solid"
+                      color="primary"
+                      shape="default"
+                      size="small"
+                      style={[a.rounded_full, a.py_sm]}
+                      onPress={() => onPressPublish()}
+                      disabled={
+                        videoState.status !== 'idle' && publishOnUpload
+                      }>
+                      <ButtonText style={[a.text_md]}>
+                        {replyTo ? (
+                          <Trans context="action">Reply</Trans>
+                        ) : (
+                          <Trans context="action">Post</Trans>
+                        )}
+                      </ButtonText>
+                    </Button>
+                  ) : (
+                    <View style={[styles.postBtn, pal.btn]}>
+                      <Text style={[pal.textLight, s.f16, s.bold]}>
+                        <Trans context="action">Post</Trans>
+                      </Text>
+                    </View>
+                  )}
+                </View>
+              )}
+            </View>
+
+            {isAltTextRequiredAndMissing && (
+              <View style={[styles.reminderLine, pal.viewLight]}>
+                <View style={styles.errorIcon}>
+                  <FontAwesomeIcon
+                    icon="exclamation"
+                    style={{color: colors.red4}}
+                    size={10}
+                  />
+                </View>
+                <Text style={[pal.text, a.flex_1]}>
+                  <Trans>One or more images is missing alt text.</Trans>
+                </Text>
               </View>
             )}
-          </View>
+            <ErrorBanner
+              error={error}
+              videoState={videoState}
+              clearError={() => setError('')}
+              clearVideo={clearVideo}
+            />
+          </Animated.View>
+          <Animated.ScrollView
+            layout={native(LinearTransition)}
+            onScroll={scrollHandler}
+            style={styles.scrollView}
+            keyboardShouldPersistTaps="always"
+            onContentSizeChange={onScrollViewContentSizeChange}
+            onLayout={onScrollViewLayout}>
+            {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
+
+            <View
+              style={[
+                styles.textInputLayout,
+                isNative && styles.textInputLayoutMobile,
+              ]}>
+              <UserAvatar
+                avatar={currentProfile?.avatar}
+                size={50}
+                type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
+              />
+              <TextInput
+                ref={textInput}
+                richtext={richtext}
+                placeholder={selectTextInputPlaceholder}
+                autoFocus
+                setRichText={setRichText}
+                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`,
+                )}
+              />
+            </View>
 
-          {isAltTextRequiredAndMissing && (
-            <View style={[styles.reminderLine, pal.viewLight]}>
-              <View style={styles.errorIcon}>
-                <FontAwesomeIcon
-                  icon="exclamation"
-                  style={{color: colors.red4}}
-                  size={10}
+            <Gallery
+              images={images}
+              dispatch={dispatch}
+              Portal={Portal.Portal}
+            />
+            {images.length === 0 && extLink && (
+              <View style={a.relative}>
+                <ExternalEmbed
+                  link={extLink}
+                  gif={extGif}
+                  onRemove={() => {
+                    if (extGif) {
+                      dispatch({type: 'embed_remove_gif'})
+                    } else {
+                      dispatch({type: 'embed_remove_link'})
+                    }
+                    setExtLink(undefined)
+                    setExtGif(undefined)
+                  }}
+                />
+                <GifAltText
+                  link={extLink}
+                  gif={extGif}
+                  onSubmit={handleChangeGifAltText}
+                  Portal={Portal.Portal}
                 />
               </View>
-              <Text style={[pal.text, a.flex_1]}>
-                <Trans>One or more images is missing alt text.</Trans>
-              </Text>
+            )}
+            <LayoutAnimationConfig skipExiting>
+              {hasVideo && (
+                <Animated.View
+                  style={[a.w_full, a.mt_lg]}
+                  entering={native(ZoomIn)}
+                  exiting={native(ZoomOut)}>
+                  {videoState.asset &&
+                    (videoState.status === 'compressing' ? (
+                      <VideoTranscodeProgress
+                        asset={videoState.asset}
+                        progress={videoState.progress}
+                        clear={clearVideo}
+                      />
+                    ) : videoState.video ? (
+                      <VideoPreview
+                        asset={videoState.asset}
+                        video={videoState.video}
+                        setDimensions={updateVideoDimensions}
+                        clear={clearVideo}
+                      />
+                    ) : null)}
+                  <SubtitleDialogBtn
+                    defaultAltText={videoState.altText}
+                    saveAltText={altText =>
+                      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,
+                        },
+                      })
+                    }}
+                    Portal={Portal.Portal}
+                  />
+                </Animated.View>
+              )}
+            </LayoutAnimationConfig>
+            <View style={!hasVideo ? [a.mt_md] : []}>
+              {quote ? (
+                <View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
+                  <View style={{pointerEvents: 'none'}}>
+                    <QuoteEmbed quote={quote} />
+                  </View>
+                  {quote.uri !== initQuote?.uri && (
+                    <QuoteX
+                      onRemove={() => {
+                        dispatch({type: 'embed_remove_quote'})
+                        setQuote(undefined)
+                      }}
+                    />
+                  )}
+                </View>
+              ) : null}
             </View>
+          </Animated.ScrollView>
+          <SuggestedLanguage text={richtext.text} />
+
+          {replyTo ? null : (
+            <ThreadgateBtn
+              postgate={postgate}
+              onChangePostgate={setPostgate}
+              threadgateAllowUISettings={threadgateAllowUISettings}
+              onChangeThreadgateAllowUISettings={
+                onChangeThreadgateAllowUISettings
+              }
+              style={bottomBarAnimatedStyle}
+              Portal={Portal.Portal}
+            />
           )}
-          <ErrorBanner
-            error={error}
-            videoState={videoState}
-            clearError={() => setError('')}
-            clearVideo={clearVideo}
-          />
-        </Animated.View>
-        <Animated.ScrollView
-          layout={native(LinearTransition)}
-          onScroll={scrollHandler}
-          style={styles.scrollView}
-          keyboardShouldPersistTaps="always"
-          onContentSizeChange={onScrollViewContentSizeChange}
-          onLayout={onScrollViewLayout}>
-          {replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
-
           <View
             style={[
-              styles.textInputLayout,
-              isNative && styles.textInputLayoutMobile,
+              t.atoms.bg,
+              t.atoms.border_contrast_medium,
+              styles.bottomBar,
             ]}>
-            <UserAvatar
-              avatar={currentProfile?.avatar}
-              size={50}
-              type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
-            />
-            <TextInput
-              ref={textInput}
-              richtext={richtext}
-              placeholder={selectTextInputPlaceholder}
-              autoFocus
-              setRichText={setRichText}
-              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`,
-              )}
-            />
-          </View>
-
-          <Gallery images={images} dispatch={dispatch} />
-          {images.length === 0 && extLink && (
-            <View style={a.relative}>
-              <ExternalEmbed
-                link={extLink}
-                gif={extGif}
-                onRemove={() => {
-                  setExtLink(undefined)
-                  setExtGif(undefined)
-                }}
-              />
-              <GifAltText
-                link={extLink}
-                gif={extGif}
-                onSubmit={handleChangeGifAltText}
-              />
-            </View>
-          )}
-          <LayoutAnimationConfig skipExiting>
-            {hasVideo && (
-              <Animated.View
-                style={[a.w_full, a.mt_lg]}
-                entering={native(ZoomIn)}
-                exiting={native(ZoomOut)}>
-                {videoState.asset &&
-                  (videoState.status === 'compressing' ? (
-                    <VideoTranscodeProgress
-                      asset={videoState.asset}
-                      progress={videoState.progress}
-                      clear={clearVideo}
-                    />
-                  ) : videoState.video ? (
-                    <VideoPreview
-                      asset={videoState.asset}
-                      video={videoState.video}
-                      setDimensions={updateVideoDimensions}
-                      clear={clearVideo}
-                    />
-                  ) : null)}
-                <SubtitleDialogBtn
-                  defaultAltText={videoAltText}
-                  saveAltText={setVideoAltText}
-                  captions={captions}
-                  setCaptions={setCaptions}
+            {videoState.status !== 'idle' && videoState.status !== 'done' ? (
+              <VideoUploadToolbar state={videoState} />
+            ) : (
+              <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
+                <SelectPhotoBtn
+                  size={images.length}
+                  disabled={!canSelectImages}
+                  onAdd={onImageAdd}
+                />
+                <SelectVideoBtn
+                  onSelectVideo={selectVideo}
+                  disabled={!canSelectImages || images?.length > 0}
+                  setError={setError}
                 />
-              </Animated.View>
+                <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
+                <SelectGifBtn
+                  onClose={focusTextInput}
+                  onSelectGif={onSelectGif}
+                  disabled={hasMedia}
+                  Portal={Portal.Portal}
+                />
+                {!isMobile ? (
+                  <Button
+                    onPress={onEmojiButtonPress}
+                    style={a.p_sm}
+                    label={_(msg`Open emoji picker`)}
+                    accessibilityHint={_(msg`Open emoji picker`)}
+                    variant="ghost"
+                    shape="round"
+                    color="primary">
+                    <EmojiSmile size="lg" />
+                  </Button>
+                ) : null}
+              </ToolbarWrapper>
             )}
-          </LayoutAnimationConfig>
-          <View style={!hasVideo ? [a.mt_md] : []}>
-            {quote ? (
-              <View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
-                <View style={{pointerEvents: 'none'}}>
-                  <QuoteEmbed quote={quote} />
-                </View>
-                {quote.uri !== initQuote?.uri && (
-                  <QuoteX onRemove={() => setQuote(undefined)} />
-                )}
-              </View>
-            ) : null}
+            <View style={a.flex_1} />
+            <SelectLangBtn />
+            <CharProgress count={graphemeLength} />
           </View>
-        </Animated.ScrollView>
-        <SuggestedLanguage text={richtext.text} />
-
-        {replyTo ? null : (
-          <ThreadgateBtn
-            postgate={postgate}
-            onChangePostgate={setPostgate}
-            threadgateAllowUISettings={threadgateAllowUISettings}
-            onChangeThreadgateAllowUISettings={
-              onChangeThreadgateAllowUISettings
-            }
-            style={bottomBarAnimatedStyle}
-          />
-        )}
-        <View
-          style={[
-            t.atoms.bg,
-            t.atoms.border_contrast_medium,
-            styles.bottomBar,
-          ]}>
-          {videoState.status !== 'idle' && videoState.status !== 'done' ? (
-            <VideoUploadToolbar state={videoState} />
-          ) : (
-            <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
-              <SelectPhotoBtn
-                size={images.length}
-                disabled={!canSelectImages}
-                onAdd={onImageAdd}
-              />
-              <SelectVideoBtn
-                onSelectVideo={selectVideo}
-                disabled={!canSelectImages || images?.length > 0}
-                setError={setError}
-              />
-              <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
-              <SelectGifBtn
-                onClose={focusTextInput}
-                onSelectGif={onSelectGif}
-                disabled={hasMedia}
-              />
-              {!isMobile ? (
-                <Button
-                  onPress={onEmojiButtonPress}
-                  style={a.p_sm}
-                  label={_(msg`Open emoji picker`)}
-                  accessibilityHint={_(msg`Open emoji picker`)}
-                  variant="ghost"
-                  shape="round"
-                  color="primary">
-                  <EmojiSmile size="lg" />
-                </Button>
-              ) : null}
-            </ToolbarWrapper>
-          )}
-          <View style={a.flex_1} />
-          <SelectLangBtn />
-          <CharProgress count={graphemeLength} />
         </View>
-      </View>
-      <Prompt.Basic
-        control={discardPromptControl}
-        title={_(msg`Discard draft?`)}
-        description={_(msg`Are you sure you'd like to discard this draft?`)}
-        onConfirm={onClose}
-        confirmButtonCta={_(msg`Discard`)}
-        confirmButtonColor="negative"
-      />
-    </KeyboardAvoidingView>
+        <Prompt.Basic
+          control={discardPromptControl}
+          title={_(msg`Discard draft?`)}
+          description={_(msg`Are you sure you'd like to discard this draft?`)}
+          onConfirm={onClose}
+          confirmButtonCta={_(msg`Discard`)}
+          confirmButtonColor="negative"
+          Portal={Portal.Portal}
+        />
+      </KeyboardAvoidingView>
+      <Portal.Outlet />
+    </Portal.Provider>
   )
 }