Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Clipclops] Add screen to view and send clip clops #3754

Merged
merged 20 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Comment on lines +24 to +26
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self: maybe we don't need this timeout

}, [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)
Comment on lines +66 to +67
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works - has been tested against fake data - but the hacked state doesn't make it great to work with. Once we have some paginated results though it will be straight forward.


// 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
Loading