Skip to content

Commit

Permalink
[PAY-1127] Improve mobile chats performance: remove index dep (#3170)
Browse files Browse the repository at this point in the history
  • Loading branch information
dharit-tan authored Apr 6, 2023
1 parent 961649a commit 0559902
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 200 deletions.
8 changes: 8 additions & 0 deletions packages/common/src/models/Chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ChatMessage } from '@audius/sdk'

import { Status } from 'models'

export type ChatMessageWithExtras = ChatMessage & {
status?: Status
hasTail: boolean
}
1 change: 1 addition & 0 deletions packages/common/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './AudioRewards'
export * from './BadgeTier'
export * from './Cache'
export * from './Chain'
export * from './Chat'
export * from './Client'
export * from './Collectible'
export * from './CollectibleState'
Expand Down
26 changes: 25 additions & 1 deletion packages/common/src/store/pages/chat/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ const { getUsers } = cacheUsersSelectors
const { selectById: selectChatById, selectAll: selectAllChats } =
chatsAdapter.getSelectors<CommonState>((state) => state.pages.chat.chats)

const { selectAll: getAllChatMessages } = chatMessagesAdapter.getSelectors()
const {
selectAll: getAllChatMessages,
selectById,
selectIds: getChatMessageIds
} = chatMessagesAdapter.getSelectors()

export const getChat = selectChatById

Expand Down Expand Up @@ -106,3 +110,23 @@ export const getOtherChatUsers = (state: CommonState, chatId?: string) => {
const chat = getChat(state, chatId)
return getOtherChatUsersFromChat(state, chat)
}

export const getChatMessageByIndex = (
state: CommonState,
chatId: string,
messageIndex: number
) => {
const chatMessagesState = state.pages.chat.messages[chatId]
const messageIds = getChatMessageIds(chatMessagesState)
const messageIdAtIndex = messageIds[messageIndex]
return selectById(chatMessagesState, messageIdAtIndex)
}

export const getChatMessageById = (
state: CommonState,
chatId: string,
messageId: string
) => {
const chatMessagesState = state.pages.chat.messages[chatId]
return selectById(chatMessagesState, messageId)
}
52 changes: 39 additions & 13 deletions packages/common/src/store/pages/chat/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,23 @@ import {
} from '@reduxjs/toolkit'
import dayjs from 'dayjs'

import { ID, Status } from 'models'
import { ID, Status, ChatMessageWithExtras } from 'models'
import { hasTail } from 'utils/chatUtils'
import { encodeHashId } from 'utils/hashIds'

type UserChatWithMessagesStatus = UserChat & {
messagesStatus?: Status
messagesSummary?: TypedCommsResponse<ChatMessage>['summary']
}

type ChatMessageWithSendStatus = ChatMessage & {
status?: Status
}

type ChatState = {
chats: EntityState<UserChatWithMessagesStatus> & {
status: Status
summary?: TypedCommsResponse<UserChat>['summary']
}
messages: Record<
string,
EntityState<ChatMessageWithSendStatus> & {
EntityState<ChatMessageWithExtras> & {
status?: Status
summary?: TypedCommsResponse<ChatMessage>['summary']
}
Expand Down Expand Up @@ -69,11 +66,10 @@ const { selectById: getChat } = chatsAdapter.getSelectors(
const messageSortComparator = (a: ChatMessage, b: ChatMessage) =>
dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? 1 : -1

export const chatMessagesAdapter =
createEntityAdapter<ChatMessageWithSendStatus>({
selectId: (message) => message.message_id,
sortComparer: messageSortComparator
})
export const chatMessagesAdapter = createEntityAdapter<ChatMessageWithExtras>({
selectId: (message) => message.message_id,
sortComparer: messageSortComparator
})

const { selectById: getMessage } = chatMessagesAdapter.getSelectors()

Expand Down Expand Up @@ -161,7 +157,22 @@ const slice = createSlice({
id: chatId,
changes: { messagesStatus: Status.SUCCESS, messagesSummary: summary }
})
chatMessagesAdapter.upsertMany(state.messages[chatId], data)
const messagesWithTail = data.map((item, index) => {
return { ...item, hasTail: hasTail(item, data[index - 1]) }
})
// Recalculate hasTail for latest message of new batch
if (state.messages[chatId].ids.length > 0) {
const prevEarliestMessageId =
state.messages[chatId].ids[state.messages[chatId].ids.length - 1]
const prevEarliestMessage =
state.messages[chatId].entities[prevEarliestMessageId]
const newLatestMessage = messagesWithTail[0]
newLatestMessage.hasTail = hasTail(
newLatestMessage,
prevEarliestMessage
)
}
chatMessagesAdapter.upsertMany(state.messages[chatId], messagesWithTail)
},
fetchMoreMessagesFailed: (
state,
Expand Down Expand Up @@ -209,7 +220,8 @@ const slice = createSlice({
reactions: [],
message: '',
sender_user_id: '',
created_at: ''
created_at: '',
hasTail: false
})
const existingMessage = getMessage(state.messages[chatId], messageId)
const existingReactions = existingMessage?.reactions ?? []
Expand Down Expand Up @@ -289,8 +301,22 @@ const slice = createSlice({
) => {
// triggers saga to get chat if not exists
const { chatId, message, status } = action.payload

// Recalculate hasTail of previous message
const prevLatestMessageId = state.messages[chatId].ids[0]
const prevLatestMessage =
state.messages[chatId].entities[prevLatestMessageId]
if (prevLatestMessage) {
const prevMsgHasTail = hasTail(prevLatestMessage, message)
chatMessagesAdapter.updateOne(state.messages[chatId], {
id: prevLatestMessageId,
changes: { hasTail: prevMsgHasTail }
})
}

chatMessagesAdapter.upsertOne(state.messages[chatId], {
...message,
hasTail: true,
status: status ?? Status.IDLE
})
chatsAdapter.updateOne(state.chats, {
Expand Down
191 changes: 100 additions & 91 deletions packages/mobile/src/screens/chat-screen/ChatMessageListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { forwardRef } from 'react'
import { memo, useCallback } from 'react'

import type { ReactionTypes } from '@audius/common'
import type { ReactionTypes, ChatMessageWithExtras } from '@audius/common'
import {
accountSelectors,
decodeHashId,
formatMessageDate
} from '@audius/common'
import type { ChatMessage, ChatMessageReaction } from '@audius/sdk'
import type { ChatMessageReaction } from '@audius/sdk'
import type { ViewStyle, StyleProp } from 'react-native'
import { View } from 'react-native'
import { TouchableWithoutFeedback } from 'react-native-gesture-handler'
Expand Down Expand Up @@ -128,101 +128,110 @@ const ChatReaction = ({ reaction }: ChatReactionProps) => {
}

type ChatMessageListItemProps = {
message: ChatMessage
hasTail: boolean
shouldShowReaction?: boolean
shouldShowDate?: boolean
message: ChatMessageWithExtras
isPopup: boolean
style?: StyleProp<ViewStyle>
onLongPress?: () => void
onLongPress?: (id: string) => void
itemsRef?: any
}

export const ChatMessageListItem = forwardRef<View, ChatMessageListItemProps>(
(props: ChatMessageListItemProps, refProp) => {
const {
message,
hasTail,
shouldShowReaction = true,
shouldShowDate = true,
style: styleProp,
onLongPress
} = props
const styles = useStyles()
const palette = useThemePalette()
export const ChatMessageListItem = memo(function ChatMessageListItem(
props: ChatMessageListItemProps
) {
const {
message,
isPopup = false,
style: styleProp,
onLongPress,
itemsRef
} = props
const styles = useStyles()
const palette = useThemePalette()

const userId = useSelector(getUserId)
const senderUserId = decodeHashId(message.sender_user_id)
const isAuthor = senderUserId === userId
const userId = useSelector(getUserId)
const senderUserId = decodeHashId(message.sender_user_id)
const isAuthor = senderUserId === userId

return (
<>
<View
style={[
isAuthor ? styles.rootIsAuthor : styles.rootOtherUser,
!hasTail && message.reactions && message.reactions.length > 0
? styles.reactionMarginBottom
: null,
styleProp
]}
>
<View>
<TouchableWithoutFeedback onPress={onLongPress}>
<View>
<View
style={[styles.bubble, isAuthor && styles.isAuthor]}
ref={refProp}
>
<Text
style={[styles.message, isAuthor && styles.messageIsAuthor]}
>
{message.message}
</Text>
</View>
{message.reactions?.length > 0 ? (
<>
{shouldShowReaction ? (
<View
style={[
styles.reactionContainer,
isAuthor
? styles.reactionContainerIsAuthor
: styles.reactionContainerOtherUser
]}
>
{message.reactions.map((reaction, index) => {
return (
<ChatReaction key={index} reaction={reaction} />
)
})}
</View>
) : null}
</>
) : null}
</View>
</TouchableWithoutFeedback>
</View>
{hasTail ? (
<>
const handleLongPress = useCallback(() => {
onLongPress?.(message.message_id)
}, [message.message_id, onLongPress])

return (
<>
<View
style={[
isAuthor ? styles.rootIsAuthor : styles.rootOtherUser,
!message.hasTail && message.reactions && message.reactions.length > 0
? styles.reactionMarginBottom
: null,
styleProp
]}
>
<View>
<TouchableWithoutFeedback onPress={handleLongPress}>
<View>
<View
style={[
styles.tail,
isAuthor ? styles.tailIsAuthor : styles.tailOtherUser,
!shouldShowDate && { bottom: 0 }
]}
style={[styles.bubble, isAuthor && styles.isAuthor]}
ref={
itemsRef
? (el) => (itemsRef.current[message.message_id] = el)
: null
}
>
<View style={styles.tailShadow} />
<ChatTail fill={isAuthor ? palette.secondary : palette.white} />
<Text
style={[styles.message, isAuthor && styles.messageIsAuthor]}
>
{message.message}
</Text>
</View>
{shouldShowDate ? (
<View style={styles.dateContainer}>
<Text style={styles.date}>
{formatMessageDate(message.created_at)}
</Text>
</View>
{message.reactions?.length > 0 ? (
<>
{!isPopup ? (
<View
style={[
styles.reactionContainer,
isAuthor
? styles.reactionContainerIsAuthor
: styles.reactionContainerOtherUser
]}
>
{message.reactions.map((reaction) => {
return (
<ChatReaction
key={reaction.created_at}
reaction={reaction}
/>
)
})}
</View>
) : null}
</>
) : null}
</>
) : null}
</View>
</TouchableWithoutFeedback>
</View>
</>
)
}
)
{message.hasTail ? (
<>
<View
style={[
styles.tail,
isAuthor ? styles.tailIsAuthor : styles.tailOtherUser,
isPopup && { bottom: 0 }
]}
>
<View style={styles.tailShadow} />
<ChatTail fill={isAuthor ? palette.secondary : palette.white} />
</View>
{!isPopup ? (
<View style={styles.dateContainer}>
<Text style={styles.date}>
{formatMessageDate(message.created_at)}
</Text>
</View>
) : null}
</>
) : null}
</View>
</>
)
})
Loading

0 comments on commit 0559902

Please sign in to comment.