about summary refs log tree commit diff
path: root/src/view/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com')
-rw-r--r--src/view/com/composer/Composer.tsx58
-rw-r--r--src/view/com/post-thread/PostThread.tsx44
-rw-r--r--src/view/com/post-thread/PostThreadComposePrompt.tsx76
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx7
4 files changed, 126 insertions, 59 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 42f057803..f5b29664a 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -45,6 +45,7 @@ import {type ImagePickerAsset} from 'expo-image-picker'
 import {
   AppBskyFeedDefs,
   type AppBskyFeedGetPostThread,
+  AppBskyUnspeccedDefs,
   type BskyAgent,
   type RichText,
 } from '@atproto/api'
@@ -55,6 +56,7 @@ import {useQueryClient} from '@tanstack/react-query'
 
 import * as apilib from '#/lib/api/index'
 import {EmbeddingDisabledError} from '#/lib/api/resolve'
+import {retry} from '#/lib/async/retry'
 import {until} from '#/lib/async/until'
 import {
   MAX_GRAPHEME_LENGTH,
@@ -87,7 +89,7 @@ import {useProfileQuery} from '#/state/queries/profile'
 import {type Gif} from '#/state/queries/tenor'
 import {useAgent, useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
-import {type ComposerOpts} from '#/state/shell/composer'
+import {type ComposerOpts, type OnPostSuccessData} from '#/state/shell/composer'
 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
 import {ComposerReplyTo} from '#/view/com/composer/ComposerReplyTo'
 import {
@@ -152,6 +154,7 @@ type Props = ComposerOpts
 export const ComposePost = ({
   replyTo,
   onPost,
+  onPostSuccess,
   quote: initQuote,
   mention: initMention,
   openEmojiPicker,
@@ -388,8 +391,10 @@ export const ComposePost = ({
     setError('')
     setIsPublishing(true)
 
-    let postUri
+    let postUri: string | undefined
+    let postSuccessData: OnPostSuccessData
     try {
+      logger.info(`composer: posting...`)
       postUri = (
         await apilib.post(agent, queryClient, {
           thread,
@@ -398,16 +403,48 @@ export const ComposePost = ({
           langs: toPostLanguages(langPrefs.postLanguage),
         })
       ).uris[0]
+
+      /*
+       * Wait for app view to have received the post(s). If this fails, it's
+       * ok, because the post _was_ actually published above.
+       */
       try {
-        await whenAppViewReady(agent, postUri, res => {
-          const postedThread = res?.data?.thread
-          return AppBskyFeedDefs.isThreadViewPost(postedThread)
-        })
+        if (postUri) {
+          logger.info(`composer: waiting for app view`)
+
+          const posts = await retry(
+            5,
+            _e => true,
+            async () => {
+              const res = await agent.app.bsky.unspecced.getPostThreadV2({
+                anchor: postUri!,
+                above: false,
+                below: thread.posts.length - 1,
+                branchingFactor: 1,
+              })
+              if (res.data.thread.length !== thread.posts.length) {
+                throw new Error(`composer: app view is not ready`)
+              }
+              if (
+                !res.data.thread.every(p =>
+                  AppBskyUnspeccedDefs.isThreadItemPost(p.value),
+                )
+              ) {
+                throw new Error(`composer: app view returned non-post items`)
+              }
+              return res.data.thread
+            },
+            1e3,
+          )
+          postSuccessData = {
+            replyToUri: replyTo?.uri,
+            posts,
+          }
+        }
       } catch (waitErr: any) {
-        logger.error(waitErr, {
-          message: `Waiting for app view failed`,
+        logger.info(`composer: waiting for app view failed`, {
+          safeMessage: waitErr,
         })
-        // Keep going because the post *was* published.
       }
     } catch (e: any) {
       logger.error(e, {
@@ -465,12 +502,14 @@ export const ComposePost = ({
           quotedThread.post.quoteCount !== initQuote.quoteCount
         ) {
           onPost?.(postUri)
+          onPostSuccess?.(postSuccessData)
           return true
         }
         return false
       })
     } else {
       onPost?.(postUri)
+      onPostSuccess?.(postSuccessData)
     }
     onClose()
     Toast.show(
@@ -489,6 +528,7 @@ export const ComposePost = ({
     langPrefs.postLanguage,
     onClose,
     onPost,
+    onPostSuccess,
     initQuote,
     replyTo,
     setLangPrefs,
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 5bec9ced1..94cc04f54 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -1,8 +1,7 @@
 import React, {memo, useRef, useState} from 'react'
-import {StyleSheet, useWindowDimensions, View} from 'react-native'
-import {runOnJS} from 'react-native-reanimated'
+import {useWindowDimensions, View} from 'react-native'
+import {runOnJS, useAnimatedStyle} from 'react-native-reanimated'
 import Animated from 'react-native-reanimated'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {
   AppBskyFeedDefs,
   type AppBskyFeedThreadgate,
@@ -13,11 +12,9 @@ import {useLingui} from '@lingui/react'
 
 import {HITSLOP_10} from '#/lib/constants'
 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
-import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
 import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
 import {useSetTitle} from '#/lib/hooks/useSetTitle'
 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {clamp} from '#/lib/numbers'
 import {ScrollProvider} from '#/lib/ScrollContext'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {cleanError} from '#/lib/strings/errors'
@@ -37,6 +34,7 @@ import {
 import {useSetThreadViewPreferencesMutation} from '#/state/queries/preferences'
 import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useSession} from '#/state/session'
+import {useShellLayout} from '#/state/shell/shell-layout'
 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
 import {useUnstablePostSource} from '#/state/unstable-post-source'
 import {List, type ListMethods} from '#/view/com/util/List'
@@ -301,11 +299,14 @@ export function PostThread({uri}: {uri: string}) {
       // maintainVisibleContentPosition and onContentSizeChange
       // to "hold onto" the correct row instead of the first one.
 
+      /*
+       * This is basically `!!parents.length`, see notes on `isParentLoading`
+       */
       if (!highlightedPost.ctx.isParentLoading && !deferParents) {
         // When progressively revealing parents, rendering a placeholder
         // here will cause scrolling jumps. Don't add it unless you test it.
         // QT'ing this thread is a great way to test all the scrolling hacks:
-        // https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o
+        // https://bsky.app/profile/samuel.bsky.team/post/3kjqhblh6qk2o
 
         // Everything is loaded
         let startIndex = Math.max(0, parents.length - maxParents)
@@ -581,6 +582,9 @@ export function PostThread({uri}: {uri: string}) {
           onEndReached={onEndReached}
           onEndReachedThreshold={2}
           onScrollToTop={onScrollToTop}
+          /**
+           * @see https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition
+           */
           maintainVisibleContentPosition={
             isNative && hasParents
               ? MAINTAIN_VISIBLE_CONTENT_POSITION
@@ -729,17 +733,16 @@ let ThreadMenu = ({
 ThreadMenu = memo(ThreadMenu)
 
 function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
-  const safeAreaInsets = useSafeAreaInsets()
-  const fabMinimalShellTransform = useMinimalShellFabTransform()
+  const {footerHeight} = useShellLayout()
+
+  const animatedStyle = useAnimatedStyle(() => {
+    return {
+      bottom: footerHeight.get(),
+    }
+  })
+
   return (
-    <Animated.View
-      style={[
-        styles.prompt,
-        fabMinimalShellTransform,
-        {
-          bottom: clamp(safeAreaInsets.bottom, 13, 60),
-        },
-      ]}>
+    <Animated.View style={[a.fixed, a.left_0, a.right_0, animatedStyle]}>
       <PostThreadComposePrompt onPressCompose={onPressReply} />
     </Animated.View>
   )
@@ -904,12 +907,3 @@ function hasBranchingReplies(node?: ThreadNode) {
   }
   return true
 }
-
-const styles = StyleSheet.create({
-  prompt: {
-    // @ts-ignore web-only
-    position: isWeb ? 'fixed' : 'absolute',
-    left: 0,
-    right: 0,
-  },
-})
diff --git a/src/view/com/post-thread/PostThreadComposePrompt.tsx b/src/view/com/post-thread/PostThreadComposePrompt.tsx
index 40acff376..f45b16085 100644
--- a/src/view/com/post-thread/PostThreadComposePrompt.tsx
+++ b/src/view/com/post-thread/PostThreadComposePrompt.tsx
@@ -1,20 +1,25 @@
-import {View} from 'react-native'
+import {type StyleProp, View, type ViewStyle} from 'react-native'
+import {LinearGradient} from 'expo-linear-gradient'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {PressableScale} from '#/lib/custom-animations/PressableScale'
 import {useHaptics} from '#/lib/haptics'
+import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, ios, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf'
+import {transparentifyColor} from '#/alf/util/colorGeneration'
 import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {Text} from '#/components/Typography'
 
 export function PostThreadComposePrompt({
   onPressCompose,
+  style,
 }: {
   onPressCompose: () => void
+  style?: StyleProp<ViewStyle>
 }) {
   const {currentAccount} = useSession()
   const {data: profile} = useProfileQuery({did: currentAccount?.did})
@@ -28,29 +33,49 @@ export function PostThreadComposePrompt({
     onOut: onHoverOut,
   } = useInteractionState()
 
+  useHideBottomBarBorderForScreen()
+
   return (
-    <PressableScale
-      accessibilityRole="button"
-      accessibilityLabel={_(msg`Compose reply`)}
-      accessibilityHint={_(msg`Opens composer`)}
+    <View
       style={[
-        gtMobile ? a.py_xs : {paddingTop: 8, paddingBottom: 11},
-        a.px_sm,
-        a.border_t,
-        t.atoms.border_contrast_low,
-        t.atoms.bg,
-      ]}
-      onPress={() => {
-        onPressCompose()
-        playHaptic('Light')
-      }}
-      onLongPress={ios(() => {
-        onPressCompose()
-        playHaptic('Heavy')
-      })}
-      onHoverIn={onHoverIn}
-      onHoverOut={onHoverOut}>
-      <View
+        gtMobile
+          ? [
+              a.py_xs,
+              a.px_sm,
+              a.border_t,
+              t.atoms.border_contrast_low,
+              t.atoms.bg,
+            ]
+          : [a.px_md, a.pb_2xs],
+        style,
+      ]}>
+      {!gtMobile && (
+        <LinearGradient
+          key={t.name} // android does not update when you change the colors. sigh.
+          start={[0.5, 0]}
+          end={[0.5, 1]}
+          colors={[
+            transparentifyColor(t.atoms.bg.backgroundColor, 0),
+            t.atoms.bg.backgroundColor,
+          ]}
+          locations={[0.15, 0.4]}
+          style={[a.absolute, a.inset_0]}
+        />
+      )}
+      <PressableScale
+        accessibilityRole="button"
+        accessibilityLabel={_(msg`Compose reply`)}
+        accessibilityHint={_(msg`Opens composer`)}
+        onPress={() => {
+          onPressCompose()
+          playHaptic('Light')
+        }}
+        onLongPress={ios(() => {
+          onPressCompose()
+          playHaptic('Heavy')
+        })}
+        onHoverIn={onHoverIn}
+        onHoverOut={onHoverOut}
         style={[
           a.flex_row,
           a.align_center,
@@ -58,6 +83,7 @@ export function PostThreadComposePrompt({
           a.gap_sm,
           a.rounded_full,
           (!gtMobile || hovered) && t.atoms.bg_contrast_25,
+          native([a.border, t.atoms.border_contrast_low]),
           a.transition_color,
         ]}>
         <UserAvatar
@@ -68,7 +94,7 @@ export function PostThreadComposePrompt({
         <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
           <Trans>Write your reply</Trans>
         </Text>
-      </View>
-    </PressableScale>
+      </PressableScale>
+    </View>
   )
 }
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 576b195a0..5184047cb 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -39,6 +39,7 @@ import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
 import {useLanguagePrefs} from '#/state/preferences'
 import {type ThreadPost} from '#/state/queries/post-thread'
 import {useSession} from '#/state/session'
+import {type OnPostSuccessData} from '#/state/shell/composer'
 import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
 import {type PostSource} from '#/state/unstable-post-source'
 import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
@@ -85,6 +86,7 @@ export function PostThreadItem({
   hasPrecedingItem,
   overrideBlur,
   onPostReply,
+  onPostSuccess,
   hideTopBorder,
   threadgateRecord,
   anchorPostSource,
@@ -103,6 +105,7 @@ export function PostThreadItem({
   hasPrecedingItem: boolean
   overrideBlur: boolean
   onPostReply: (postUri: string | undefined) => void
+  onPostSuccess?: (data: OnPostSuccessData) => void
   hideTopBorder?: boolean
   threadgateRecord?: AppBskyFeedThreadgate.Record
   anchorPostSource?: PostSource
@@ -139,6 +142,7 @@ export function PostThreadItem({
         hasPrecedingItem={hasPrecedingItem}
         overrideBlur={overrideBlur}
         onPostReply={onPostReply}
+        onPostSuccess={onPostSuccess}
         hideTopBorder={hideTopBorder}
         threadgateRecord={threadgateRecord}
         anchorPostSource={anchorPostSource}
@@ -185,6 +189,7 @@ let PostThreadItemLoaded = ({
   hasPrecedingItem,
   overrideBlur,
   onPostReply,
+  onPostSuccess,
   hideTopBorder,
   threadgateRecord,
   anchorPostSource,
@@ -204,6 +209,7 @@ let PostThreadItemLoaded = ({
   hasPrecedingItem: boolean
   overrideBlur: boolean
   onPostReply: (postUri: string | undefined) => void
+  onPostSuccess?: (data: OnPostSuccessData) => void
   hideTopBorder?: boolean
   threadgateRecord?: AppBskyFeedThreadgate.Record
   anchorPostSource?: PostSource
@@ -298,6 +304,7 @@ let PostThreadItemLoaded = ({
         moderation,
       },
       onPost: onPostReply,
+      onPostSuccess: onPostSuccess,
     })
   }