[Clipclops] Add screen to view and send clip clops (#3754)
* add new routes with placeholder screens * add clops list * add a clop input * add some better padding to the clops * some more adjustments * add rnkc * implement rnkc * implement rnkc * be a little less weird about it * rename clop stuff * rename more clop * one more * [Clipclops] Temp codegenerated lexicon (#3749) * add codegenerated lexicon * replace hailey's types * use codegen'd types in components * fix error + throw if fetch failed * remove bad imports * update messageslist and messageitem * import useState * add clop service URL hook * add dm service url storage * use context * use context for service url (temp) * remove log * nits --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
parent
5d19f27052
commit
eb8bfd11d1
28 changed files with 1295 additions and 7 deletions
65
src/screens/Messages/Conversation/MessageInput.tsx
Normal file
65
src/screens/Messages/Conversation/MessageInput.tsx
Normal file
|
@ -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>
|
||||
)
|
||||
}
|
29
src/screens/Messages/Conversation/MessageItem.tsx
Normal file
29
src/screens/Messages/Conversation/MessageItem.tsx
Normal file
|
@ -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>
|
||||
)
|
||||
}
|
193
src/screens/Messages/Conversation/MessagesList.tsx
Normal file
193
src/screens/Messages/Conversation/MessagesList.tsx
Normal file
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue