about summary refs log tree commit diff
path: root/src/screens/Messages/Conversation/MessagesList.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens/Messages/Conversation/MessagesList.tsx')
-rw-r--r--src/screens/Messages/Conversation/MessagesList.tsx454
1 files changed, 0 insertions, 454 deletions
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
deleted file mode 100644
index 3034f0290..000000000
--- a/src/screens/Messages/Conversation/MessagesList.tsx
+++ /dev/null
@@ -1,454 +0,0 @@
-import React, {useCallback, useRef} from 'react'
-import {FlatList, LayoutChangeEvent, View} from 'react-native'
-import {
-  KeyboardStickyView,
-  useKeyboardHandler,
-} from 'react-native-keyboard-controller'
-import {
-  runOnJS,
-  scrollTo,
-  useAnimatedRef,
-  useAnimatedStyle,
-  useSharedValue,
-} from 'react-native-reanimated'
-import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
-
-import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
-import {
-  convertBskyAppUrlIfNeeded,
-  isBskyPostUrl,
-} from '#/lib/strings/url-helpers'
-import {logger} from '#/logger'
-import {isNative} from '#/platform/detection'
-import {isConvoActive, useConvoActive} from '#/state/messages/convo'
-import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
-import {useGetPost} from '#/state/queries/post'
-import {useAgent} from '#/state/session'
-import {clamp} from 'lib/numbers'
-import {ScrollProvider} from 'lib/ScrollContext'
-import {isWeb} from 'platform/detection'
-import {
-  EmojiPicker,
-  EmojiPickerState,
-} from '#/view/com/composer/text-input/web/EmojiPicker.web'
-import {List} from 'view/com/util/List'
-import {ChatDisabled} from '#/screens/Messages/Conversation/ChatDisabled'
-import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
-import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
-import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
-import {MessageItem} from '#/components/dms/MessageItem'
-import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
-import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
-
-function MaybeLoader({isLoading}: {isLoading: boolean}) {
-  return (
-    <View
-      style={{
-        height: 50,
-        width: '100%',
-        alignItems: 'center',
-        justifyContent: 'center',
-      }}>
-      {isLoading && <Loader size="xl" />}
-    </View>
-  )
-}
-
-function renderItem({item}: {item: ConvoItem}) {
-  if (item.type === 'message' || item.type === 'pending-message') {
-    return <MessageItem item={item} />
-  } else if (item.type === 'deleted-message') {
-    return <Text>Deleted message</Text>
-  } else if (item.type === 'error') {
-    return <MessageListError item={item} />
-  }
-
-  return null
-}
-
-function keyExtractor(item: ConvoItem) {
-  return item.key
-}
-
-function onScrollToIndexFailed() {
-  // Placeholder function. You have to give FlatList something or else it will error.
-}
-
-export function MessagesList({
-  hasScrolled,
-  setHasScrolled,
-  blocked,
-  footer,
-}: {
-  hasScrolled: boolean
-  setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
-  blocked?: boolean
-  footer?: React.ReactNode
-}) {
-  const convoState = useConvoActive()
-  const agent = useAgent()
-  const getPost = useGetPost()
-  const {embedUri, setEmbed} = useMessageEmbed()
-
-  const flatListRef = useAnimatedRef<FlatList>()
-
-  const [newMessagesPill, setNewMessagesPill] = React.useState({
-    show: false,
-    startContentOffset: 0,
-  })
-
-  const [emojiPickerState, setEmojiPickerState] =
-    React.useState<EmojiPickerState>({
-      isOpen: false,
-      pos: {top: 0, left: 0, right: 0, bottom: 0},
-    })
-
-  // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items
-  // are added to the list. For example, if the user is scrolled up to 1iew older messages, we don't want to scroll to
-  // the bottom.
-  const isAtBottom = useSharedValue(true)
-
-  // This will be used on web to assist in determining if we need to maintain the content offset
-  const isAtTop = useSharedValue(true)
-
-  // Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing
-  // onStartReached to fire.
-  const prevContentHeight = useRef(0)
-  const prevItemCount = useRef(0)
-
-  // -- Keep track of background state and positioning for new pill
-  const layoutHeight = useSharedValue(0)
-  const didBackground = React.useRef(false)
-  React.useEffect(() => {
-    if (convoState.status === ConvoStatus.Backgrounded) {
-      didBackground.current = true
-    }
-  }, [convoState.status])
-
-  // -- Scroll handling
-
-  // Every time the content size changes, that means one of two things is happening:
-  // 1. New messages are being added from the log or from a message you have sent
-  // 2. Old messages are being prepended to the top
-  //
-  // The first time that the content size changes is when the initial items are rendered. Because we cannot rely on
-  // `initialScrollIndex`, we need to immediately scroll to the bottom of the list. That scroll will not be animated.
-  //
-  // Subsequent resizes will only scroll to the bottom if the user is at the bottom of the list (within 100 pixels of
-  // the bottom). Therefore, any new messages that come in or are sent will result in an animated scroll to end. However
-  // we will not scroll whenever new items get prepended to the top.
-  const onContentSizeChange = useCallback(
-    (_: number, height: number) => {
-      // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
-      // previous off whenever we add new content to the previous offset whenever we add new content to the list.
-      if (isWeb && isAtTop.value && hasScrolled) {
-        flatListRef.current?.scrollToOffset({
-          offset: height - prevContentHeight.current,
-          animated: false,
-        })
-      }
-
-      // This number _must_ be the height of the MaybeLoader component
-      if (height > 50 && isAtBottom.value) {
-        // If the size of the content is changing by more than the height of the screen, then we don't
-        // want to scroll further than the start of all the new content. Since we are storing the previous offset,
-        // we can just scroll the user to that offset and add a little bit of padding. We'll also show the pill
-        // that can be pressed to immediately scroll to the end.
-        if (
-          didBackground.current &&
-          hasScrolled &&
-          height - prevContentHeight.current > layoutHeight.value - 50 &&
-          convoState.items.length - prevItemCount.current > 1
-        ) {
-          flatListRef.current?.scrollToOffset({
-            offset: prevContentHeight.current - 65,
-            animated: true,
-          })
-          setNewMessagesPill({
-            show: true,
-            startContentOffset: prevContentHeight.current - 65,
-          })
-        } else {
-          flatListRef.current?.scrollToOffset({
-            offset: height,
-            animated: hasScrolled && height > prevContentHeight.current,
-          })
-
-          // HACK Unfortunately, we need to call `setHasScrolled` after a brief delay,
-          // because otherwise there is too much of a delay between the time the content
-          // scrolls and the time the screen appears, causing a flicker.
-          // We cannot actually use a synchronous scroll here, because `onContentSizeChange`
-          // is actually async itself - all the info has to come across the bridge first.
-          if (!hasScrolled && !convoState.isFetchingHistory) {
-            setTimeout(() => {
-              setHasScrolled(true)
-            }, 100)
-          }
-        }
-      }
-
-      prevContentHeight.current = height
-      prevItemCount.current = convoState.items.length
-      didBackground.current = false
-    },
-    [
-      hasScrolled,
-      setHasScrolled,
-      convoState.isFetchingHistory,
-      convoState.items.length,
-      // these are stable
-      flatListRef,
-      isAtTop.value,
-      isAtBottom.value,
-      layoutHeight.value,
-    ],
-  )
-
-  const onStartReached = useCallback(() => {
-    if (hasScrolled && prevContentHeight.current > layoutHeight.value) {
-      convoState.fetchMessageHistory()
-    }
-  }, [convoState, hasScrolled, layoutHeight.value])
-
-  const onScroll = React.useCallback(
-    (e: ReanimatedScrollEvent) => {
-      'worklet'
-      layoutHeight.value = e.layoutMeasurement.height
-      const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height
-
-      // Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom
-      // when a new message is added, hence the 100 pixel offset
-      isAtBottom.value = e.contentSize.height - 100 < bottomOffset
-      isAtTop.value = e.contentOffset.y <= 1
-
-      if (
-        newMessagesPill.show &&
-        (e.contentOffset.y > newMessagesPill.startContentOffset + 200 ||
-          isAtBottom.value)
-      ) {
-        runOnJS(setNewMessagesPill)({
-          show: false,
-          startContentOffset: 0,
-        })
-      }
-    },
-    [layoutHeight, newMessagesPill, isAtBottom, isAtTop],
-  )
-
-  // -- Keyboard animation handling
-  const {bottom: bottomInset} = useSafeAreaInsets()
-  const bottomOffset = isWeb ? 0 : clamp(60 + bottomInset, 60, 75)
-
-  const keyboardHeight = useSharedValue(0)
-  const keyboardIsOpening = useSharedValue(false)
-
-  // In some cases - like when the emoji piker opens - we don't want to animate the scroll in the list onLayout event.
-  // We use this value to keep track of when we want to disable the animation.
-  const layoutScrollWithoutAnimation = useSharedValue(false)
-
-  useKeyboardHandler({
-    onStart: e => {
-      'worklet'
-      // Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should
-      // just update the height here instead of having the `onMove` event do it (that event will not fire!)
-      if (e.duration === 0) {
-        layoutScrollWithoutAnimation.value = true
-        keyboardHeight.value = e.height
-      } else {
-        keyboardIsOpening.value = true
-      }
-    },
-    onMove: e => {
-      'worklet'
-      keyboardHeight.value = e.height
-      if (e.height > bottomOffset) {
-        scrollTo(flatListRef, 0, 1e7, false)
-      }
-    },
-    onEnd: () => {
-      'worklet'
-      keyboardIsOpening.value = false
-    },
-  })
-
-  const animatedListStyle = useAnimatedStyle(() => ({
-    marginBottom:
-      keyboardHeight.value > bottomOffset ? keyboardHeight.value : bottomOffset,
-  }))
-
-  // -- Message sending
-  const onSendMessage = useCallback(
-    async (text: string) => {
-      let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true})
-
-      // detect facets without resolution first - this is used to see if there's
-      // any post links in the text that we can embed. We do this first because
-      // we want to remove the post link from the text, re-trim, then detect facets
-      rt.detectFacetsWithoutResolution()
-
-      let embed: AppBskyEmbedRecord.Main | undefined
-
-      if (embedUri) {
-        try {
-          const post = await getPost({uri: embedUri})
-          if (post) {
-            embed = {
-              $type: 'app.bsky.embed.record',
-              record: {
-                uri: post.uri,
-                cid: post.cid,
-              },
-            }
-
-            // look for the embed uri in the facets, so we can remove it from the text
-            const postLinkFacet = rt.facets?.find(facet => {
-              return facet.features.find(feature => {
-                if (AppBskyRichtextFacet.isLink(feature)) {
-                  if (isBskyPostUrl(feature.uri)) {
-                    const url = convertBskyAppUrlIfNeeded(feature.uri)
-                    const [_0, _1, _2, rkey] = url.split('/').filter(Boolean)
-
-                    // this might have a handle instead of a DID
-                    // so just compare the rkey - not particularly dangerous
-                    return post.uri.endsWith(rkey)
-                  }
-                }
-                return false
-              })
-            })
-
-            if (postLinkFacet) {
-              const isAtStart = postLinkFacet.index.byteStart === 0
-              const isAtEnd =
-                postLinkFacet.index.byteEnd === rt.unicodeText.graphemeLength
-
-              // remove the post link from the text
-              if (isAtStart || isAtEnd) {
-                rt.delete(
-                  postLinkFacet.index.byteStart,
-                  postLinkFacet.index.byteEnd,
-                )
-              }
-
-              rt = new RichText({text: rt.text.trim()}, {cleanNewlines: true})
-            }
-          }
-        } catch (error) {
-          logger.error('Failed to get post as quote for DM', {error})
-        }
-      }
-
-      await rt.detectFacets(agent)
-
-      rt = shortenLinks(rt)
-      rt = stripInvalidMentions(rt)
-
-      if (!hasScrolled) {
-        setHasScrolled(true)
-      }
-
-      convoState.sendMessage({
-        text: rt.text,
-        facets: rt.facets,
-        embed,
-      })
-    },
-    [agent, convoState, embedUri, getPost, hasScrolled, setHasScrolled],
-  )
-
-  // -- List layout changes (opening emoji keyboard, etc.)
-  const onListLayout = React.useCallback(
-    (e: LayoutChangeEvent) => {
-      layoutHeight.value = e.nativeEvent.layout.height
-
-      if (isWeb || !keyboardIsOpening.value) {
-        flatListRef.current?.scrollToEnd({
-          animated: !layoutScrollWithoutAnimation.value,
-        })
-        layoutScrollWithoutAnimation.value = false
-      }
-    },
-    [
-      flatListRef,
-      keyboardIsOpening.value,
-      layoutScrollWithoutAnimation,
-      layoutHeight,
-    ],
-  )
-
-  const scrollToEndOnPress = React.useCallback(() => {
-    flatListRef.current?.scrollToOffset({
-      offset: prevContentHeight.current,
-      animated: true,
-    })
-  }, [flatListRef])
-
-  return (
-    <>
-      {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
-      <ScrollProvider onScroll={onScroll}>
-        <List
-          ref={flatListRef}
-          data={convoState.items}
-          renderItem={renderItem}
-          keyExtractor={keyExtractor}
-          disableFullWindowScroll={true}
-          disableVirtualization={true}
-          style={animatedListStyle}
-          // The extra two items account for the header and the footer components
-          initialNumToRender={isNative ? 32 : 62}
-          maxToRenderPerBatch={isWeb ? 32 : 62}
-          keyboardDismissMode="on-drag"
-          keyboardShouldPersistTaps="handled"
-          maintainVisibleContentPosition={{
-            minIndexForVisible: 0,
-          }}
-          removeClippedSubviews={false}
-          sideBorders={false}
-          onContentSizeChange={onContentSizeChange}
-          onLayout={onListLayout}
-          onStartReached={onStartReached}
-          onScrollToIndexFailed={onScrollToIndexFailed}
-          scrollEventThrottle={100}
-          ListHeaderComponent={
-            <MaybeLoader isLoading={convoState.isFetchingHistory} />
-          }
-        />
-      </ScrollProvider>
-      <KeyboardStickyView offset={{closed: -bottomOffset, opened: 0}}>
-        {convoState.status === ConvoStatus.Disabled ? (
-          <ChatDisabled />
-        ) : blocked ? (
-          footer
-        ) : (
-          <>
-            {isConvoActive(convoState) &&
-              !convoState.isFetchingHistory &&
-              convoState.items.length === 0 && <ChatEmptyPill />}
-            <MessageInput
-              onSendMessage={onSendMessage}
-              hasEmbed={!!embedUri}
-              setEmbed={setEmbed}
-              openEmojiPicker={pos => setEmojiPickerState({isOpen: true, pos})}>
-              <MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
-            </MessageInput>
-          </>
-        )}
-      </KeyboardStickyView>
-
-      {isWeb && (
-        <EmojiPicker
-          pinToTop
-          state={emojiPickerState}
-          close={() => setEmojiPickerState(prev => ({...prev, isOpen: false}))}
-        />
-      )}
-
-      {newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />}
-    </>
-  )
-}