@planningcenter/chat-react-native 3.18.0-rc.5 → 3.18.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +2 -1
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversations/conversations.d.ts.map +1 -1
  5. package/build/components/conversations/conversations.js +2 -3
  6. package/build/components/conversations/conversations.js.map +1 -1
  7. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  8. package/build/hooks/use_conversation_messages_jolt_events.js +23 -70
  9. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  10. package/build/hooks/use_message_create_or_update.d.ts +0 -2
  11. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  12. package/build/hooks/use_message_create_or_update.js +10 -8
  13. package/build/hooks/use_message_create_or_update.js.map +1 -1
  14. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  15. package/build/screens/conversation_details_screen.js +3 -4
  16. package/build/screens/conversation_details_screen.js.map +1 -1
  17. package/build/screens/conversation_new/components/form_list.d.ts +2 -2
  18. package/build/screens/conversation_new/components/form_list.d.ts.map +1 -1
  19. package/build/screens/conversation_new/components/form_list.js +2 -3
  20. package/build/screens/conversation_new/components/form_list.js.map +1 -1
  21. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.d.ts.map +1 -1
  22. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js +2 -3
  23. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js.map +1 -1
  24. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.d.ts.map +1 -1
  25. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js +2 -3
  26. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js.map +1 -1
  27. package/build/types/jolt_events/reaction_events.d.ts +1 -0
  28. package/build/types/jolt_events/reaction_events.d.ts.map +1 -1
  29. package/build/types/jolt_events/reaction_events.js.map +1 -1
  30. package/build/utils/cache/messages_cache.d.ts +9 -0
  31. package/build/utils/cache/messages_cache.d.ts.map +1 -0
  32. package/build/utils/cache/messages_cache.js +89 -0
  33. package/build/utils/cache/messages_cache.js.map +1 -0
  34. package/build/utils/cache/optimistically_create_message.d.ts +2 -1
  35. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  36. package/build/utils/cache/optimistically_create_message.js +6 -3
  37. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  38. package/package.json +2 -3
  39. package/src/components/conversation/message.tsx +2 -1
  40. package/src/components/conversations/conversations.tsx +7 -9
  41. package/src/hooks/use_conversation_messages_jolt_events.ts +39 -81
  42. package/src/hooks/use_message_create_or_update.ts +10 -9
  43. package/src/screens/conversation_details_screen.tsx +3 -4
  44. package/src/screens/conversation_new/components/form_list.tsx +3 -5
  45. package/src/screens/conversation_select_recipients/conversation_select_group_recipients_screen.tsx +2 -4
  46. package/src/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.tsx +2 -4
  47. package/src/types/jolt_events/reaction_events.ts +1 -0
  48. package/src/utils/cache/messages_cache.ts +113 -0
  49. package/src/utils/cache/optimistically_create_message.ts +7 -2
@@ -1,7 +1,6 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
- import { FlashList } from '@shopify/flash-list'
3
2
  import React, { useMemo } from 'react'
4
- import { StyleSheet, View } from 'react-native'
3
+ import { FlatList, StyleSheet, View } from 'react-native'
5
4
  import { useConversationsContext } from '../../contexts/conversations_context'
6
5
  import { useTheme } from '../../hooks'
7
6
  import { ConversationResource } from '../../types'
@@ -35,7 +34,7 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
35
34
 
36
35
  const showBadges = !chat_group_graph_id
37
36
 
38
- const data: FlashListItem[] = useMemo(() => {
37
+ const data: FlatListItem[] = useMemo(() => {
39
38
  if (isLoading) {
40
39
  return loadingPlaceholder
41
40
  }
@@ -54,9 +53,8 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
54
53
 
55
54
  return (
56
55
  <View style={styles.container}>
57
- <FlashList
56
+ <FlatList
58
57
  data={data}
59
- estimatedItemSize={97}
60
58
  keyExtractor={item => item.id.toString()}
61
59
  contentContainerStyle={styles.contentContainer}
62
60
  onRefresh={refetch}
@@ -114,18 +112,18 @@ const useStyles = () => {
114
112
  })
115
113
  }
116
114
 
117
- interface FlashListLoadingItem {
115
+ interface FlatListLoadingItem {
118
116
  type: 'loading'
119
117
  id: string
120
118
  }
121
- interface FlashListConversationItem {
119
+ interface FlatListConversationItem {
122
120
  type: 'conversation'
123
121
  resource: ConversationResource
124
122
  id: number
125
123
  }
126
- type FlashListItem = FlashListLoadingItem | FlashListConversationItem
124
+ type FlatListItem = FlatListLoadingItem | FlatListConversationItem
127
125
 
128
- const loadingPlaceholder: FlashListItem[] = Array.from({ length: 5 }, (_, i) => ({
126
+ const loadingPlaceholder: FlatListItem[] = Array.from({ length: 5 }, (_, i) => ({
129
127
  type: 'loading',
130
128
  id: `loading${i}`,
131
129
  }))
@@ -1,22 +1,21 @@
1
1
  import { ApiCollection, MessageResource } from '../types'
2
2
  import { useJoltChannel, useJoltEvent } from './use_jolt'
3
- import {
4
- deleteRecordInPagesData,
5
- updateOrCreateRecordInPagesData,
6
- updateRecordInPagesData,
7
- } from '../utils'
3
+ import { deleteRecordInPagesData } from '../utils'
8
4
  import { MessageCreatedEvent, MessageDeletedEvent } from '../types/jolt_events/message_events'
9
5
  import { InfiniteData, useQueryClient } from '@tanstack/react-query'
10
6
  import { useCurrentPerson } from './use_current_person'
11
7
  import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'
12
8
  import { getRequestQueryKey } from './use_suspense_api'
13
9
  import { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'
14
- import { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'
15
10
  import { getMessagesRequestArgs } from '../utils/request/get_messages'
16
11
  import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
17
- import { isTemporaryMessageId } from './use_message_create_or_update'
18
12
  import { completeMessageCreationTracking } from '../utils/performance_tracking'
19
13
  import { useApiClient } from './use_api_client'
14
+ import {
15
+ updateCacheWithMessage,
16
+ updateCacheWithReaction,
17
+ getThreadedMessagesQueryKey,
18
+ } from '../utils/cache/messages_cache'
20
19
 
21
20
  interface Props {
22
21
  conversationId: number
@@ -47,42 +46,17 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
47
46
  }
48
47
  }
49
48
 
50
- queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {
51
- if (e.event === 'message.created') {
52
- // Before adding the new message, remove any pending temporary messages
53
- // with matching text to prevent duplicates from race conditions
54
- let dataAfterTempRemoval = prev
55
- if (prev && message.text && message.mine) {
56
- dataAfterTempRemoval = deleteRecordInPagesData({
57
- data: prev,
58
- record: message,
59
- matchFn: (existingMessage, _record) => {
60
- return (
61
- isTemporaryMessageId(existingMessage.id) &&
62
- existingMessage.text === message.text &&
63
- existingMessage.mine
64
- )
65
- },
66
- })
67
- }
68
-
69
- return updateOrCreateRecordInPagesData({
70
- data: dataAfterTempRemoval,
71
- record: message,
72
- processRecord: (record, current) => {
73
- return { ...current, ...record }
74
- },
75
- })
76
- } else {
77
- return updateRecordInPagesData({
78
- data: prev,
79
- record: message,
80
- processRecord: (record, current) => {
81
- return { ...current, ...record }
82
- },
83
- })
84
- }
85
- })
49
+ // Update the main conversation cache
50
+ updateCacheWithMessage(queryClient, messagesQueryKey, message, e.event)
51
+
52
+ // If message has a reply_root_id, also update the threaded cache
53
+ if (data.reply_root_id) {
54
+ const threadedMessagesQueryKey = getThreadedMessagesQueryKey(
55
+ conversationId,
56
+ data.reply_root_id
57
+ )
58
+ updateCacheWithMessage(queryClient, threadedMessagesQueryKey, message, e.event)
59
+ }
86
60
  }
87
61
 
88
62
  const handleMessageDeleted = async (e: MessageDeletedEvent) => {
@@ -92,50 +66,34 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
92
66
  currentPersonId: currentPerson.id,
93
67
  })
94
68
 
69
+ // Update the main conversation cache
95
70
  queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>
96
71
  deleteRecordInPagesData({ data: prev, record: message })
97
72
  )
73
+
74
+ // If message has a reply_root_id, also update the threaded cache
75
+ if (data.reply_root_id) {
76
+ const threadedMessagesQueryKey = getThreadedMessagesQueryKey(
77
+ conversationId,
78
+ data.reply_root_id
79
+ )
80
+ queryClient.setQueryData<QueryData>(threadedMessagesQueryKey, prev =>
81
+ deleteRecordInPagesData({ data: prev, record: message })
82
+ )
83
+ }
98
84
  }
99
85
 
100
86
  const handleReactionJoltEvent = async (e: JoltReactionEvent) => {
101
- const { data } = e.data
102
- const message = { id: data.message_sort_key } as MessageResource
103
- queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>
104
- updateRecordInPagesData({
105
- data: prev,
106
- record: message,
107
- processRecord: (record, oldMessage) => {
108
- const reactionCounts = oldMessage.reactionCounts || []
109
- let foundMatch = false
110
- let newReactionCounts = reactionCounts.map(reactionCount => {
111
- if (reactionCount.value === data.value) {
112
- foundMatch = true
113
- return transformReactionEventDataToReactionCountResource({
114
- data,
115
- oldData: reactionCount,
116
- event: e.event,
117
- currentPersonId: currentPerson.id,
118
- })
119
- }
120
- return reactionCount
121
- })
122
-
123
- if (!foundMatch) {
124
- const newReactionCount = transformReactionEventDataToReactionCountResource({
125
- data,
126
- event: e.event,
127
- currentPersonId: currentPerson.id,
128
- })
129
-
130
- if (newReactionCount?.count) {
131
- newReactionCounts = [...newReactionCounts, newReactionCount]
132
- }
133
- }
134
-
135
- return { ...oldMessage, reactionCounts: newReactionCounts }
136
- },
137
- })
138
- )
87
+ // Update the main conversation cache and capture the reply_root_id if present
88
+ updateCacheWithReaction(queryClient, messagesQueryKey, e, currentPerson.id)
89
+
90
+ const replyRootId = e.data.data.reply_root_id
91
+
92
+ // If the message has a reply_root_id, also update the threaded cache
93
+ if (replyRootId) {
94
+ const threadedMessagesQueryKey = getThreadedMessagesQueryKey(conversationId, replyRootId)
95
+ updateCacheWithReaction(queryClient, threadedMessagesQueryKey, e, currentPerson.id)
96
+ }
139
97
  }
140
98
 
141
99
  const handleTypingEvent = async (e: JoltTypingEvent) => {
@@ -14,6 +14,7 @@ import { useCurrentPerson } from './use_current_person'
14
14
  import { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'
15
15
  import { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'
16
16
  import { startMessageCreationTracking } from '../utils/performance_tracking'
17
+ import { isNewMessage } from '../utils/cache/messages_cache'
17
18
 
18
19
  interface Props {
19
20
  conversationId: number
@@ -93,6 +94,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
93
94
  text,
94
95
  attachments,
95
96
  currentPerson,
97
+ replyRootId,
96
98
  })
97
99
 
98
100
  return { message: optimisticMessage }
@@ -103,7 +105,10 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
103
105
 
104
106
  // Add error to the optimistic message from the cache on error
105
107
  if (optimisticMessage) {
106
- const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
108
+ const queryKey = getMessagesQueryKey({
109
+ conversation_id: conversationId,
110
+ reply_root_id: replyRootId,
111
+ })
107
112
  chatQueryClient.setQueryData(
108
113
  queryKey,
109
114
  (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>
@@ -123,7 +128,10 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
123
128
  const { message: optimisticMessage } = context || {}
124
129
  const updatedMessage = result.data
125
130
  type QueryData = InfiniteData<ApiCollection<MessageResource>>
126
- const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
131
+ const queryKey = getMessagesQueryKey({
132
+ conversation_id: conversationId,
133
+ reply_root_id: replyRootId,
134
+ })
127
135
 
128
136
  // First remove the optimistic message if it exists
129
137
  if (optimisticMessage) {
@@ -148,13 +156,6 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
148
156
  return mutation
149
157
  }
150
158
 
151
- export function isTemporaryMessageId(messageId?: string | null): boolean {
152
- return !!messageId && messageId.endsWith('-temp')
153
- }
154
- export function isNewMessage(message?: MessageResource): boolean {
155
- return !message?.id || isTemporaryMessageId(message.id)
156
- }
157
-
158
159
  /**
159
160
  * Generate a random UUID (v4) for idempotent keys.
160
161
  * Uses Math.random, which is not cryptographically secure.
@@ -9,6 +9,7 @@ import React, {
9
9
  useRef,
10
10
  } from 'react'
11
11
  import {
12
+ FlatList,
12
13
  StyleSheet,
13
14
  TextInput,
14
15
  View,
@@ -40,7 +41,6 @@ import {
40
41
  } from '../hooks/use_conversation'
41
42
  import { MemberResource, isDefined } from '../types'
42
43
  import { HeaderTextButton } from '../components/display/platform_modal_header_buttons'
43
- import { FlashList } from '@shopify/flash-list'
44
44
  import { tokens } from '../vendor/tapestry/tokens'
45
45
  import { ButtonAppearanceUnion } from '../components/display/utils/button_colors'
46
46
  import { GroupResource } from '../types/resources/group_resource'
@@ -300,9 +300,8 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
300
300
 
301
301
  return (
302
302
  <View style={styles.listContainer}>
303
- <FlashList
303
+ <FlatList
304
304
  data={listData as SectionListData}
305
- estimatedItemSize={52}
306
305
  contentContainerStyle={styles.contentContainer}
307
306
  renderItem={({ item, index }) => {
308
307
  const [isStart, isEnd] = [
@@ -375,7 +374,7 @@ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenPr
375
374
  return null
376
375
  }
377
376
  }}
378
- onEndReached={fetchNextPageOfMembers}
377
+ onEndReached={() => fetchNextPageOfMembers()}
379
378
  />
380
379
  </View>
381
380
  )
@@ -1,6 +1,5 @@
1
1
  import React from 'react'
2
- import { StyleSheet, View } from 'react-native'
3
- import { FlashList, type FlashListProps } from '@shopify/flash-list'
2
+ import { FlatList, type FlatListProps, StyleSheet, View } from 'react-native'
4
3
  import { MemberResource } from '../../../types'
5
4
  import { Person, Text } from '../../../components/display'
6
5
  import { useTheme } from '../../../hooks'
@@ -8,7 +7,7 @@ import { useTheme } from '../../../hooks'
8
7
  interface FormListProps {
9
8
  memberData: MemberResource[]
10
9
  loadingMore?: boolean
11
- FormContent?: FlashListProps<MemberResource>['ListHeaderComponent']
10
+ FormContent?: FlatListProps<MemberResource>['ListHeaderComponent']
12
11
  listEmptyText?: string
13
12
  onEndReached?: () => void
14
13
  }
@@ -23,7 +22,7 @@ export const FormList = ({
23
22
  const styles = useStyles()
24
23
 
25
24
  return (
26
- <FlashList
25
+ <FlatList
27
26
  data={memberData}
28
27
  ListHeaderComponent={FormContent}
29
28
  renderItem={({ item }) => <Person person={item} style={styles.person} />}
@@ -31,7 +30,6 @@ export const FormList = ({
31
30
  loadingMore ? <Text style={styles.loadingMore}>Loading more...</Text> : null
32
31
  }
33
32
  keyExtractor={item => item.id.toString()}
34
- estimatedItemSize={45}
35
33
  ListEmptyComponent={<ListEmptyText text={listEmptyText || 'No members found'} />}
36
34
  onEndReached={onEndReached}
37
35
  />
@@ -1,7 +1,6 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import React from 'react'
3
- import { StyleSheet, View } from 'react-native'
4
- import { FlashList } from '@shopify/flash-list'
3
+ import { FlatList, StyleSheet, View } from 'react-native'
5
4
  import { Heading } from '../../components'
6
5
  import { GroupsGroupResource } from '../../types'
7
6
  import { useGroupsGroups } from '../../hooks/use_groups_groups'
@@ -31,10 +30,9 @@ export const ConversationSelectGroupRecipientsScreen = ({
31
30
  }
32
31
 
33
32
  return (
34
- <FlashList
33
+ <FlatList
35
34
  data={groupsWithCreatePermission}
36
35
  keyExtractor={item => item.id.toString()}
37
- estimatedItemSize={65}
38
36
  contentContainerStyle={styles.contentContainer}
39
37
  ListHeaderComponent={
40
38
  <View style={styles.sectionHeader}>
@@ -1,7 +1,6 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import React from 'react'
3
- import { StyleSheet, View } from 'react-native'
4
- import { FlashList } from '@shopify/flash-list'
3
+ import { FlatList, StyleSheet, View } from 'react-native'
5
4
  import { Heading } from '../../components'
6
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
7
6
  import { ConversationSelectRecipientsScreenProps } from './types/screen_props'
@@ -30,10 +29,9 @@ export const ConversationSelectTeamsILeadRecipientsScreen = ({
30
29
  }
31
30
 
32
31
  return (
33
- <FlashList
32
+ <FlatList
34
33
  data={serviceTypes}
35
34
  keyExtractor={item => item.id.toString()}
36
- estimatedItemSize={65}
37
35
  contentContainerStyle={styles.contentContainer}
38
36
  ListHeaderComponent={
39
37
  <View style={styles.sectionHeader}>
@@ -6,6 +6,7 @@ interface BaseReactionEventData extends Record<string, unknown> {
6
6
  author_id: number
7
7
  conversation_id: number
8
8
  message_sort_key: string
9
+ reply_root_id?: string | null
9
10
  created_at: string
10
11
  organization_id: number
11
12
  value: ReactionCountResource['value']
@@ -0,0 +1,113 @@
1
+ import { InfiniteData, QueryClient } from '@tanstack/react-query'
2
+ import { ApiCollection, MessageResource } from '../../types'
3
+ import { deleteRecordInPagesData } from './page_mutations'
4
+ import { updateOrCreateRecordInPagesData, updateRecordInPagesData } from './page_mutations'
5
+ import { JoltReactionEvent } from '../../types/jolt_events'
6
+ import { transformReactionEventDataToReactionCountResource } from '../jolt/transform_reaction_event_data_to_reaction_count_resource'
7
+ import { getMessagesRequestArgs } from '../request/get_messages'
8
+ import { getRequestQueryKey } from '../../hooks/use_suspense_api'
9
+
10
+ export function updateCacheWithMessage(
11
+ queryClient: QueryClient,
12
+ queryKey: unknown[],
13
+ message: MessageResource,
14
+ event: 'message.created' | 'message.updated'
15
+ ) {
16
+ queryClient.setQueryData<MessagesQueryData>(queryKey, prev => {
17
+ if (event === 'message.created') {
18
+ // Before adding the new message, remove any pending temporary messages
19
+ // with matching text to prevent duplicates from race conditions
20
+ let dataAfterTempRemoval = prev
21
+ if (prev && message.text && message.mine) {
22
+ dataAfterTempRemoval = deleteRecordInPagesData({
23
+ data: prev,
24
+ record: message,
25
+ matchFn: (existingMessage, _record) => {
26
+ return (
27
+ isTemporaryMessageId(existingMessage.id) &&
28
+ existingMessage.text === message.text &&
29
+ existingMessage.mine
30
+ )
31
+ },
32
+ })
33
+ }
34
+
35
+ return updateOrCreateRecordInPagesData({
36
+ data: dataAfterTempRemoval,
37
+ record: message,
38
+ processRecord: (record, current) => {
39
+ return { ...current, ...record }
40
+ },
41
+ })
42
+ } else {
43
+ return updateRecordInPagesData({
44
+ data: prev,
45
+ record: message,
46
+ processRecord: (record, current) => {
47
+ return { ...current, ...record }
48
+ },
49
+ })
50
+ }
51
+ })
52
+ }
53
+
54
+ export function updateCacheWithReaction(
55
+ queryClient: QueryClient,
56
+ queryKey: unknown[],
57
+ event: JoltReactionEvent,
58
+ currentPersonId: number
59
+ ) {
60
+ const message = { id: event.data.data.message_sort_key } as MessageResource
61
+ queryClient.setQueryData<MessagesQueryData>(queryKey, prev =>
62
+ updateRecordInPagesData({
63
+ data: prev,
64
+ record: message,
65
+ processRecord: (record, oldMessage) => {
66
+ const reactionCounts = oldMessage.reactionCounts || []
67
+ let foundMatch = false
68
+ let newReactionCounts = reactionCounts.map(reactionCount => {
69
+ if (reactionCount.value === event.data.data.value) {
70
+ foundMatch = true
71
+ return transformReactionEventDataToReactionCountResource({
72
+ data: event.data.data,
73
+ oldData: reactionCount,
74
+ event: event.event,
75
+ currentPersonId,
76
+ })
77
+ }
78
+ return reactionCount
79
+ })
80
+
81
+ if (!foundMatch) {
82
+ const newReactionCount = transformReactionEventDataToReactionCountResource({
83
+ data: event.data.data,
84
+ event: event.event,
85
+ currentPersonId,
86
+ })
87
+
88
+ if (newReactionCount?.count) {
89
+ newReactionCounts = [...newReactionCounts, newReactionCount]
90
+ }
91
+ }
92
+
93
+ return { ...oldMessage, reactionCounts: newReactionCounts }
94
+ },
95
+ })
96
+ )
97
+ }
98
+
99
+ type MessagesQueryData = InfiniteData<ApiCollection<MessageResource>>
100
+ export function isTemporaryMessageId(messageId?: string | null): boolean {
101
+ return !!messageId && messageId.endsWith('-temp')
102
+ }
103
+ export function isNewMessage(message?: MessageResource): boolean {
104
+ return !message?.id || isTemporaryMessageId(message.id)
105
+ }
106
+
107
+ export function getThreadedMessagesQueryKey(conversationId: number, replyRootId: string) {
108
+ const requestArgs = getMessagesRequestArgs({
109
+ conversation_id: conversationId,
110
+ reply_root_id: replyRootId,
111
+ })
112
+ return getRequestQueryKey(requestArgs)
113
+ }
@@ -14,12 +14,14 @@ export function optimisticallyCreateMessage({
14
14
  attachments,
15
15
  currentPerson,
16
16
  message,
17
+ replyRootId,
17
18
  }: {
18
19
  conversationId: number
19
20
  text: string
20
21
  attachments?: DenormalizedAttachmentResourceForCreate[]
21
22
  currentPerson: CurrentPersonResource
22
23
  message?: MessageResource
24
+ replyRootId?: string | null
23
25
  }) {
24
26
  const id = message?.id || generateTempMessageId()
25
27
 
@@ -49,12 +51,15 @@ export function optimisticallyCreateMessage({
49
51
  lastInGroup: true,
50
52
  pending: true,
51
53
  replyCount: 0,
52
- replyRootId: null,
54
+ replyRootId: replyRootId || null,
53
55
  }
54
56
 
55
57
  // Add the optimistic message to the cache
56
58
  type QueryData = InfiniteData<ApiCollection<MessageResource>>
57
- const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
59
+ const queryKey = getMessagesQueryKey({
60
+ conversation_id: conversationId,
61
+ reply_root_id: replyRootId,
62
+ })
58
63
 
59
64
  chatQueryClient.setQueryData<QueryData>(queryKey, data =>
60
65
  updateOrCreateRecordInPagesData({