diff options
Diffstat (limited to 'src/screens/Messages/Conversation')
-rw-r--r-- | src/screens/Messages/Conversation/MessageInput.tsx | 65 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/MessageItem.tsx | 29 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/MessagesList.tsx | 193 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/index.tsx | 10 |
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> ) } |