Skip to content

Commit

Permalink
[Clipclops] Add screen to view and send clip clops (#3754)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
haileyok and mozzius authored Apr 30, 2024
1 parent 5d19f27 commit eb8bfd1
Show file tree
Hide file tree
Showing 28 changed files with 1,295 additions and 7 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "^0.38.1",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.11.7",
"react-native-pager-view": "6.2.3",
"react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress",
Expand Down
5 changes: 4 additions & 1 deletion src/App.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'view/icons'

import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {KeyboardProvider} from 'react-native-keyboard-controller'
import {RootSiblingParent} from 'react-native-root-siblings'
import {
initialWindowMetrics,
Expand Down Expand Up @@ -137,7 +138,9 @@ function App() {
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
<KeyboardProvider>
<InnerApp />
</KeyboardProvider>
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
Expand Down
65 changes: 65 additions & 0 deletions src/screens/Messages/Conversation/MessageInput.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions src/screens/Messages/Conversation/MessageItem.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 193 additions & 0 deletions src/screens/Messages/Conversation/MessagesList.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
10 changes: 6 additions & 4 deletions src/screens/Messages/Conversation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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'

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<
Expand All @@ -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>
)
}
2 changes: 1 addition & 1 deletion src/screens/Messages/List/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function MessagesListScreen({}: Props) {
renderItem={({item}) => {
return (
<Link
to={`/messages/${item.profile.handle}`}
to={`/messages/3kqzb4mytxk2v`}
style={[a.flex_1, a.pl_md, a.py_sm, a.gap_md, a.pr_2xl]}>
<PreviewableUserAvatar profile={item.profile} size={44} />
<View style={[a.flex_1]}>
Expand Down
Loading

0 comments on commit eb8bfd1

Please sign in to comment.