Skip to content

Commit

Permalink
[Clipclops] 2 Clipped 2 Clopped (#3796)
Browse files Browse the repository at this point in the history
* Add new pkg

* copy queries over to new file

* useConvoQuery

* useListConvos

* Use useListConvos

* extract useConvoQuery

* useGetConvoForMembers

* Delete unused

* exract useListConvos

* Replace imports

* Messages/List/index.tsx

* extract getconvoformembers

* MessageItem

* delete chatLog and rename query.ts

* Update import

* Clipclop service (#3794)

* Add Chat service

* Better handle deletions

* Rollback unneeded changes

* Better insertion order

* Use clipclops

* don't show FAB if error

* clean up imports

* Update Convo service

* Remove temp files

---------

Co-authored-by: Samuel Newman <[email protected]>
  • Loading branch information
estrattonbailey and mozzius authored May 1, 2024
1 parent d61b366 commit 538ca8d
Show file tree
Hide file tree
Showing 30 changed files with 752 additions and 1,130 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto-labs/api": "^0.12.8-clipclops.0",
"@atproto/api": "^0.12.5",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
Expand Down
6 changes: 3 additions & 3 deletions src/components/dms/NewChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast'
Expand All @@ -17,7 +18,6 @@ import {atoms as a, useTheme, web} from '#/alf'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {useGetChatFromMembers} from '../../screens/Messages/Temp/query/query'
import {Button} from '../Button'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '../icons/Envelope'
import {ListMaybePlaceholder} from '../Lists'
Expand All @@ -33,9 +33,9 @@ export function NewChat({
const t = useTheme()
const {_} = useLingui()

const {mutate: createChat} = useGetChatFromMembers({
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
onNewChat(data.chat.id)
onNewChat(data.convo.id)
},
onError: error => {
Toast.show(error.message)
Expand Down
15 changes: 9 additions & 6 deletions src/screens/Messages/Conversation/MessageItem.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
import React, {useCallback, useMemo} from 'react'
import {StyleProp, TextStyle, View} from 'react-native'
import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'

import {useSession} from '#/state/session'
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as TempDmChatDefs from '#/temp/dm/defs'

export function MessageItem({
item,
next,
}: {
item: TempDmChatDefs.MessageView
next: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage | null
item: ChatBskyConvoDefs.MessageView
next:
| ChatBskyConvoDefs.MessageView
| ChatBskyConvoDefs.DeletedMessageView
| null
}) {
const t = useTheme()
const {currentAccount} = useSession()

const isFromSelf = item.sender?.did === currentAccount?.did

const isNextFromSelf =
TempDmChatDefs.isMessageView(next) &&
ChatBskyConvoDefs.isMessageView(next) &&
next.sender?.did === currentAccount?.did

const isLastInGroup = useMemo(() => {
Expand All @@ -32,7 +35,7 @@ export function MessageItem({
}

// or, if there's a 10 minute gap between this message and the next
if (TempDmChatDefs.isMessageView(next)) {
if (ChatBskyConvoDefs.isMessageView(next)) {
const thisDate = new Date(item.sentAt)
const nextDate = new Date(next.sentAt)

Expand Down Expand Up @@ -88,7 +91,7 @@ function Metadata({
isLastInGroup,
style,
}: {
message: TempDmChatDefs.MessageView
message: ChatBskyConvoDefs.MessageView
isLastInGroup: boolean
style: StyleProp<TextStyle>
}) {
Expand Down
180 changes: 61 additions & 119 deletions src/screens/Messages/Conversation/MessagesList.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import React, {useCallback, useMemo, useRef, useState} from 'react'
import React, {useCallback, useMemo, useRef} from 'react'
import {FlatList, View, ViewToken} from 'react-native'
import {Alert} from 'react-native'
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'

import {isWeb} from '#/platform/detection'
import {useChat} from '#/state/messages'
import {ChatProvider} from '#/state/messages'
import {ConvoItem, ConvoStatus} from '#/state/messages/convo'
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'

type MessageWithNext = {
message: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage
next: TempDmChatDefs.MessageView | TempDmChatDefs.DeletedMessage | null
}

function MaybeLoader({isLoading}: {isLoading: boolean}) {
return (
Expand All @@ -34,47 +25,43 @@ function MaybeLoader({isLoading}: {isLoading: boolean}) {
)
}

function renderItem({item}: {item: MessageWithNext}) {
if (TempDmChatDefs.isMessageView(item.message))
return <MessageItem item={item.message} next={item.next} />

if (TempDmChatDefs.isDeletedMessage(item)) return <Text>Deleted message</Text>
function renderItem({item}: {item: ConvoItem}) {
if (item.type === 'message') {
return <MessageItem item={item.message} next={item.nextMessage} />
} else if (item.type === 'deleted-message') {
return <Text>Deleted message</Text>
} else if (item.type === 'pending-message') {
return <Text>{item.message.text}</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)
export function MessagesList({convoId}: {convoId: string}) {
return (
<ChatProvider convoId={convoId}>
<MessagesListInner />
</ChatProvider>
)
}

export function MessagesListInner() {
const chat = useChat()
const flatListRef = useRef<FlatList>(null)
// 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)
// 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>}) => {
Expand All @@ -93,108 +80,63 @@ export function MessagesList({chatId}: {chatId: string}) {
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)
}, [])
chat.service.fetchMessageHistory()
}, [chat])

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)
)
},
)
.reduce((acc, message) => {
// convert [n1, n2, n3, ...] to [{message: n1, next: n2}, {message: n2, next: n3}, {message: n3, next: n4}, ...]

return [...acc, {message, next: acc.at(-1)?.message ?? null}]
}, [] as MessageWithNext[])
totalMessages.current = filtered.length

return filtered
}, [chat])

return (
<KeyboardAvoidingView
style={{flex: 1, marginBottom: isWeb ? 20 : 85}}
behavior="padding"
keyboardVerticalOffset={70}
contentContainerStyle={{flex: 1}}>
<FlatList
data={messages}
keyExtractor={item => item.message.id}
renderItem={renderItem}
contentContainerStyle={{paddingHorizontal: 10}}
inverted={true}
// 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}
removeClippedSubviews={true}
onEndReached={onEndReached}
onScrollToIndexFailed={onScrollToEndFailed}
onContentSizeChange={onContentSizeChange}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
maintainVisibleContentPosition={{
minIndexForVisible: 1,
}}
ListFooterComponent={<MaybeLoader isLoading={false} />}
ref={flatListRef}
keyboardDismissMode="none"
/>
{chat.state.status === ConvoStatus.Ready && (
<FlatList
data={chat.state.items}
keyExtractor={item => item.key}
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,
}}
ListFooterComponent={
<MaybeLoader isLoading={chat.state.isFetchingHistory} />
}
removeClippedSubviews={true}
ref={flatListRef}
keyboardDismissMode="none"
/>
)}

<View style={{paddingHorizontal: 10}}>
<MessageInput
onSendMessage={onSendMessage}
onSendMessage={text => {
chat.service.sendMessage({
text,
})
}}
onFocus={onInputFocus}
onBlur={onInputBlur}
/>
Expand Down
8 changes: 4 additions & 4 deletions src/screens/Messages/Conversation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'

import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {useConvoQuery} from '#/state/queries/messages/conversation'
import {BACK_HITSLOP} from 'lib/constants'
import {isWeb} from 'platform/detection'
import {useSession} from 'state/session'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {CenteredView} from 'view/com/util/Views'
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
import {useChatQuery} from '#/screens/Messages/Temp/query/query'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {DotGrid_Stroke2_Corner0_Rounded} from '#/components/icons/DotGrid'
Expand All @@ -29,11 +29,11 @@ type Props = NativeStackScreenProps<
>
export function MessagesConversationScreen({route}: Props) {
const gate = useGate()
const chatId = route.params.conversation
const convoId = route.params.conversation
const {currentAccount} = useSession()
const myDid = currentAccount?.did

const {data: chat, isError: isError} = useChatQuery(chatId)
const {data: chat, isError: isError} = useConvoQuery(convoId)
const otherProfile = React.useMemo(() => {
return chat?.members?.find(m => m.did !== myDid)
}, [chat?.members, myDid])
Expand All @@ -51,7 +51,7 @@ export function MessagesConversationScreen({route}: Props) {
return (
<CenteredView style={{flex: 1}} sideBorders>
<Header profile={otherProfile} />
<MessagesList chatId={chatId} />
<MessagesList convoId={convoId} />
</CenteredView>
)
}
Expand Down
Loading

0 comments on commit 538ca8d

Please sign in to comment.