about summary refs log tree commit diff
path: root/src/screens/Messages/Conversation
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens/Messages/Conversation')
-rw-r--r--src/screens/Messages/Conversation/MessageInput.tsx65
-rw-r--r--src/screens/Messages/Conversation/MessageItem.tsx29
-rw-r--r--src/screens/Messages/Conversation/MessagesList.tsx193
-rw-r--r--src/screens/Messages/Conversation/index.tsx10
4 files changed, 293 insertions, 4 deletions
diff --git a/src/screens/Messages/Conversation/MessageInput.tsx b/src/screens/Messages/Conversation/MessageInput.tsx
new file mode 100644
index 000000000..bd73594ce
--- /dev/null
+++ b/src/screens/Messages/Conversation/MessageInput.tsx
@@ -0,0 +1,65 @@
+import React from 'react'
+import {Pressable, TextInput, View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+
+export function MessageInput({
+  onSendMessage,
+  onFocus,
+  onBlur,
+}: {
+  onSendMessage: (message: string) => void
+  onFocus: () => void
+  onBlur: () => void
+}) {
+  const t = useTheme()
+  const [message, setMessage] = React.useState('')
+
+  const inputRef = React.useRef<TextInput>(null)
+
+  const onSubmit = React.useCallback(() => {
+    onSendMessage(message)
+    setMessage('')
+    setTimeout(() => {
+      inputRef.current?.focus()
+    }, 100)
+  }, [message, onSendMessage])
+
+  return (
+    <View
+      style={[
+        a.flex_row,
+        a.py_sm,
+        a.px_sm,
+        a.rounded_full,
+        a.mt_sm,
+        t.atoms.bg_contrast_25,
+      ]}>
+      <TextInput
+        accessibilityLabel="Text input field"
+        accessibilityHint="Write a message"
+        value={message}
+        onChangeText={setMessage}
+        placeholder="Write a message"
+        style={[a.flex_1, a.text_sm, a.px_sm]}
+        onSubmitEditing={onSubmit}
+        onFocus={onFocus}
+        onBlur={onBlur}
+        placeholderTextColor={t.palette.contrast_500}
+        ref={inputRef}
+      />
+      <Pressable
+        accessibilityRole="button"
+        style={[
+          a.rounded_full,
+          a.align_center,
+          a.justify_center,
+          {height: 30, width: 30, backgroundColor: t.palette.primary_500},
+        ]}
+        onPress={onSubmit}>
+        <Text style={a.text_md}>🐴</Text>
+      </Pressable>
+    </View>
+  )
+}
diff --git a/src/screens/Messages/Conversation/MessageItem.tsx b/src/screens/Messages/Conversation/MessageItem.tsx
new file mode 100644
index 000000000..74e65488e
--- /dev/null
+++ b/src/screens/Messages/Conversation/MessageItem.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+import * as TempDmChatDefs from '#/temp/dm/defs'
+
+export function MessageItem({item}: {item: TempDmChatDefs.MessageView}) {
+  const t = useTheme()
+
+  return (
+    <View
+      style={[
+        a.py_sm,
+        a.px_md,
+        a.my_xs,
+        a.rounded_md,
+        {
+          backgroundColor: t.palette.primary_500,
+          maxWidth: '65%',
+          borderRadius: 17,
+        },
+      ]}>
+      <Text style={[a.text_md, {lineHeight: 1.2, color: 'white'}]}>
+        {item.text}
+      </Text>
+    </View>
+  )
+}
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>
+  )
+}
diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx
index 239425a2f..efa64f5f8 100644
--- a/src/screens/Messages/Conversation/index.tsx
+++ b/src/screens/Messages/Conversation/index.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {View} from 'react-native'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -7,6 +6,8 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {CommonNavigatorParams} from '#/lib/routes/types'
 import {useGate} from '#/lib/statsig/statsig'
 import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {CenteredView} from 'view/com/util/Views'
+import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
 import {ClipClopGate} from '../gate'
 
 type Props = NativeStackScreenProps<
@@ -16,17 +17,18 @@ type Props = NativeStackScreenProps<
 export function MessagesConversationScreen({route}: Props) {
   const chatId = route.params.conversation
   const {_} = useLingui()
-
   const gate = useGate()
+
   if (!gate('dms')) return <ClipClopGate />
 
   return (
-    <View>
+    <CenteredView style={{flex: 1}} sideBorders>
       <ViewHeader
         title={_(msg`Chat with ${chatId}`)}
         showOnDesktop
         showBorder
       />
-    </View>
+      <MessagesList chatId={chatId} />
+    </CenteredView>
   )
 }