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.tsx193
1 files changed, 193 insertions, 0 deletions
diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx
new file mode 100644
index 000000000..aafed42af
--- /dev/null
+++ b/src/screens/Messages/Conversation/MessagesList.tsx
@@ -0,0 +1,193 @@
+import React, {useCallback, useMemo, useRef, useState} from 'react'
+import {Alert, FlatList, View, ViewToken} from 'react-native'
+import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
+
+import {isWeb} from 'platform/detection'
+import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
+import {MessageItem} from '#/screens/Messages/Conversation/MessageItem'
+import {
+  useChat,
+  useChatLogQuery,
+  useSendMessageMutation,
+} from '#/screens/Messages/Temp/query/query'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
+import * as TempDmChatDefs from '#/temp/dm/defs'
+
+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: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage
+}) {
+  if (TempDmChatDefs.isMessageView(item)) return <MessageItem item={item} />
+
+  if (TempDmChatDefs.isDeletedMessage(item)) return <Text>Deleted message</Text>
+
+  return null
+}
+
+// TODO rm
+// TEMP: This is a temporary function to generate unique keys for mutation placeholders
+const generateUniqueKey = () => `_${Math.random().toString(36).substr(2, 9)}`
+
+function onScrollToEndFailed() {
+  // Placeholder function. You have to give FlatList something or else it will error.
+}
+
+export function MessagesList({chatId}: {chatId: string}) {
+  const flatListRef = useRef<FlatList>(null)
+
+  // Whenever we reach the end (visually the top), we don't want to keep calling it. We will set `isFetching` to true
+  // once the request for new posts starts. Then, we will change it back to false after the content size changes.
+  const isFetching = useRef(false)
+
+  // We use this to know if we should scroll after a new clop is added to the list
+  const isAtBottom = useRef(false)
+
+  // Because the viewableItemsChanged callback won't have access to the updated state, we use a ref to store the
+  // total number of clops
+  // TODO this needs to be set to whatever the initial number of messages is
+  const totalMessages = useRef(10)
+
+  // TODO later
+  const [_, setShowSpinner] = useState(false)
+
+  // Query Data
+  const {data: chat} = useChat(chatId)
+  const {mutate: sendMessage} = useSendMessageMutation(chatId)
+  useChatLogQuery()
+
+  const [onViewableItemsChanged, viewabilityConfig] = useMemo(() => {
+    return [
+      (info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
+        const firstVisibleIndex = info.viewableItems[0]?.index
+
+        isAtBottom.current = Number(firstVisibleIndex) < 2
+      },
+      {
+        itemVisiblePercentThreshold: 50,
+        minimumViewTime: 10,
+      },
+    ]
+  }, [])
+
+  const onContentSizeChange = useCallback(() => {
+    if (isAtBottom.current) {
+      flatListRef.current?.scrollToOffset({offset: 0, animated: true})
+    }
+
+    isFetching.current = false
+    setShowSpinner(false)
+  }, [])
+
+  const onEndReached = useCallback(() => {
+    if (isFetching.current) return
+    isFetching.current = true
+    setShowSpinner(true)
+
+    // Eventually we will add more here when we hit the top through RQuery
+    // We wouldn't actually use a timeout, but there would be a delay while loading
+    setTimeout(() => {
+      // Do something
+      setShowSpinner(false)
+    }, 1000)
+  }, [])
+
+  const onInputFocus = useCallback(() => {
+    if (!isAtBottom.current) {
+      flatListRef.current?.scrollToOffset({offset: 0, animated: true})
+    }
+  }, [])
+
+  const onSendMessage = useCallback(
+    async (message: string) => {
+      if (!message) return
+
+      try {
+        sendMessage({
+          message,
+          tempId: generateUniqueKey(),
+        })
+      } catch (e: any) {
+        Alert.alert(e.toString())
+      }
+    },
+    [sendMessage],
+  )
+
+  const onInputBlur = useCallback(() => {}, [])
+
+  const messages = useMemo(() => {
+    if (!chat) return []
+
+    const filtered = chat.messages.filter(
+      (
+        message,
+      ): message is
+        | TempDmChatDefs.MessageView
+        | TempDmChatDefs.DeletedMessage => {
+        return (
+          TempDmChatDefs.isMessageView(message) ||
+          TempDmChatDefs.isDeletedMessage(message)
+        )
+      },
+    )
+    totalMessages.current = filtered.length
+  }, [chat])
+
+  return (
+    <KeyboardAvoidingView
+      style={{flex: 1, marginBottom: isWeb ? 20 : 85}}
+      behavior="padding"
+      keyboardVerticalOffset={70}
+      contentContainerStyle={{flex: 1}}>
+      <FlatList
+        data={messages}
+        keyExtractor={item => item.id}
+        renderItem={renderItem}
+        contentContainerStyle={{paddingHorizontal: 10}}
+        // In the future, we might want to adjust this value. Not very concerning right now as long as we are only
+        // dealing with text. But whenever we have images or other media and things are taller, we will want to lower
+        // this...probably
+        initialNumToRender={20}
+        // Same with the max to render per batch. Let's be safe for now though.
+        maxToRenderPerBatch={25}
+        inverted={true}
+        onEndReached={onEndReached}
+        onScrollToIndexFailed={onScrollToEndFailed}
+        onContentSizeChange={onContentSizeChange}
+        onViewableItemsChanged={onViewableItemsChanged}
+        viewabilityConfig={viewabilityConfig}
+        maintainVisibleContentPosition={{
+          minIndexForVisible: 0,
+        }}
+        // This is actually a header since we are inverted!
+        ListFooterComponent={<MaybeLoader isLoading={false} />}
+        removeClippedSubviews={true}
+        ref={flatListRef}
+        keyboardDismissMode="none"
+      />
+      <View style={{paddingHorizontal: 10}}>
+        <MessageInput
+          onSendMessage={onSendMessage}
+          onFocus={onInputFocus}
+          onBlur={onInputBlur}
+        />
+      </View>
+    </KeyboardAvoidingView>
+  )
+}