@planningcenter/chat-react-native 4.2.1-qa-staging.1 → 4.2.1-qa-staging.4

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 (93) hide show
  1. package/build/components/conversation/message_form.d.ts.map +1 -1
  2. package/build/components/conversation/message_form.js +1 -1
  3. package/build/components/conversation/message_form.js.map +1 -1
  4. package/build/contexts/conversation_context.d.ts +3 -1
  5. package/build/contexts/conversation_context.d.ts.map +1 -1
  6. package/build/contexts/conversation_context.js +23 -18
  7. package/build/contexts/conversation_context.js.map +1 -1
  8. package/build/hooks/use_conversation_message_lookup.d.ts +7 -0
  9. package/build/hooks/use_conversation_message_lookup.d.ts.map +1 -0
  10. package/build/hooks/use_conversation_message_lookup.js +52 -0
  11. package/build/hooks/use_conversation_message_lookup.js.map +1 -0
  12. package/build/hooks/use_conversation_messages.d.ts +1 -0
  13. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  14. package/build/hooks/use_conversation_messages.js +8 -1
  15. package/build/hooks/use_conversation_messages.js.map +1 -1
  16. package/build/hooks/use_conversation_messages_jolt_events.d.ts +12 -4
  17. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  18. package/build/hooks/use_conversation_messages_jolt_events.js +82 -88
  19. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  20. package/build/hooks/use_fetch_newer_messages.d.ts.map +1 -1
  21. package/build/hooks/use_fetch_newer_messages.js +27 -21
  22. package/build/hooks/use_fetch_newer_messages.js.map +1 -1
  23. package/build/hooks/use_fetch_newer_on_foreground.d.ts +1 -1
  24. package/build/hooks/use_fetch_newer_on_foreground.d.ts.map +1 -1
  25. package/build/hooks/use_fetch_newer_on_foreground.js +2 -1
  26. package/build/hooks/use_fetch_newer_on_foreground.js.map +1 -1
  27. package/build/hooks/use_held_back_messages.d.ts +11 -0
  28. package/build/hooks/use_held_back_messages.d.ts.map +1 -0
  29. package/build/hooks/use_held_back_messages.js +24 -0
  30. package/build/hooks/use_held_back_messages.js.map +1 -0
  31. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  32. package/build/hooks/use_message_create_or_update.js +91 -80
  33. package/build/hooks/use_message_create_or_update.js.map +1 -1
  34. package/build/hooks/use_message_reaction_toggle.d.ts.map +1 -1
  35. package/build/hooks/use_message_reaction_toggle.js +45 -40
  36. package/build/hooks/use_message_reaction_toggle.js.map +1 -1
  37. package/build/screens/conversation_new/components/services_form.js.map +1 -1
  38. package/build/screens/conversation_screen.d.ts.map +1 -1
  39. package/build/screens/conversation_screen.js +495 -157
  40. package/build/screens/conversation_screen.js.map +1 -1
  41. package/build/screens/message_actions_screen.d.ts.map +1 -1
  42. package/build/screens/message_actions_screen.js +21 -26
  43. package/build/screens/message_actions_screen.js.map +1 -1
  44. package/build/screens/message_report_screen.d.ts.map +1 -1
  45. package/build/screens/message_report_screen.js +17 -44
  46. package/build/screens/message_report_screen.js.map +1 -1
  47. package/build/screens/reactions_screen.d.ts.map +1 -1
  48. package/build/screens/reactions_screen.js +70 -88
  49. package/build/screens/reactions_screen.js.map +1 -1
  50. package/build/utils/cache/find_message_in_caches.d.ts +4 -0
  51. package/build/utils/cache/find_message_in_caches.d.ts.map +1 -0
  52. package/build/utils/cache/find_message_in_caches.js +16 -0
  53. package/build/utils/cache/find_message_in_caches.js.map +1 -0
  54. package/build/utils/cache/messages_cache.d.ts +1 -0
  55. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  56. package/build/utils/cache/messages_cache.js +3 -0
  57. package/build/utils/cache/messages_cache.js.map +1 -1
  58. package/build/utils/cache/optimistically_update_message.d.ts +2 -1
  59. package/build/utils/cache/optimistically_update_message.d.ts.map +1 -1
  60. package/build/utils/cache/optimistically_update_message.js +4 -4
  61. package/build/utils/cache/optimistically_update_message.js.map +1 -1
  62. package/build/utils/cache/prepend_newer_page.d.ts +1 -1
  63. package/build/utils/cache/prepend_newer_page.d.ts.map +1 -1
  64. package/build/utils/cache/prepend_newer_page.js +10 -9
  65. package/build/utils/cache/prepend_newer_page.js.map +1 -1
  66. package/package.json +3 -3
  67. package/src/__tests__/hooks/use_conversation_messages.test.ts +22 -0
  68. package/src/__tests__/hooks/use_conversation_messages_jolt_events.test.ts +22 -25
  69. package/src/components/conversation/message_form.tsx +1 -2
  70. package/src/contexts/conversation_context.tsx +6 -1
  71. package/src/hooks/__tests__/use_fetch_newer_messages.test.tsx +68 -0
  72. package/src/hooks/__tests__/use_held_back_messages.test.tsx +60 -0
  73. package/src/hooks/__tests__/use_message_reaction_toggle.test.tsx +73 -0
  74. package/src/hooks/use_conversation_message_lookup.ts +29 -0
  75. package/src/hooks/use_conversation_messages.ts +9 -0
  76. package/src/hooks/use_conversation_messages_jolt_events.ts +43 -41
  77. package/src/hooks/use_fetch_newer_messages.ts +19 -12
  78. package/src/hooks/use_fetch_newer_on_foreground.ts +2 -2
  79. package/src/hooks/use_held_back_messages.ts +33 -0
  80. package/src/hooks/use_message_create_or_update.ts +8 -8
  81. package/src/hooks/use_message_reaction_toggle.ts +3 -1
  82. package/src/screens/conversation_new/components/services_form.tsx +1 -1
  83. package/src/screens/conversation_screen.tsx +21 -20
  84. package/src/screens/message_actions_screen.tsx +3 -5
  85. package/src/screens/message_report_screen.tsx +2 -3
  86. package/src/screens/reactions_screen.tsx +2 -4
  87. package/src/utils/cache/__tests__/find_message_in_caches.test.ts +37 -0
  88. package/src/utils/cache/__tests__/messages_cache.test.ts +14 -1
  89. package/src/utils/cache/__tests__/prepend_newer_page.test.ts +13 -14
  90. package/src/utils/cache/find_message_in_caches.ts +20 -0
  91. package/src/utils/cache/messages_cache.ts +3 -0
  92. package/src/utils/cache/optimistically_update_message.ts +2 -5
  93. package/src/utils/cache/prepend_newer_page.ts +7 -10
@@ -2,10 +2,11 @@ import { useQueryClient } from '@tanstack/react-query'
2
2
  import { useCallback } from 'react'
3
3
  import { ApiCollection, MessageResource } from '../types'
4
4
  import { prependNewerPage } from '../utils/cache/prepend_newer_page'
5
- import { RequestData } from '../utils/client'
6
5
  import { getMessagesRequestArgs } from '../utils/request/get_messages'
7
6
  import { useApiClient } from './use_api_client'
8
7
 
8
+ const MAX_PAGES = 5
9
+
9
10
  type Options = {
10
11
  conversation_id: number
11
12
  reply_root_id?: string | null
@@ -17,20 +18,26 @@ export const useFetchNewerMessages = ({ conversation_id, reply_root_id, queryKey
17
18
  const apiClient = useApiClient()
18
19
 
19
20
  return useCallback(
20
- (newestMessageId: string) => {
21
+ async (newestMessageId: string) => {
21
22
  const { url, data } = getMessagesRequestArgs({ conversation_id, reply_root_id })
22
- const where = { id_gt: newestMessageId }
23
- const pageParam: Partial<RequestData> = { where, order: 'asc' }
23
+ const pages: ApiCollection<MessageResource>[] = []
24
+ let cursor: string | undefined = newestMessageId
24
25
 
25
- return apiClient.chat
26
- .get<ApiCollection<MessageResource>>({ url, data: { ...data, where, order: 'asc' } })
27
- .then(page => {
28
- queryClient.setQueryData<Parameters<typeof prependNewerPage<MessageResource>>[0]>(
29
- queryKey,
30
- prev => prependNewerPage(prev, page, pageParam)
31
- )
26
+ while (cursor && pages.length < MAX_PAGES) {
27
+ const page: ApiCollection<MessageResource> = await apiClient.chat.get({
28
+ url,
29
+ data: { ...data, where: { id_gt: cursor }, order: 'asc' },
32
30
  })
33
- .catch(() => {})
31
+ pages.push(page)
32
+ cursor = page.meta?.next?.idGt
33
+ }
34
+
35
+ queryClient.setQueryData<Parameters<typeof prependNewerPage<MessageResource>>[0]>(
36
+ queryKey,
37
+ prev => prependNewerPage(prev, pages)
38
+ )
39
+ const moreThanWeFetched = !!cursor
40
+ if (moreThanWeFetched) await queryClient.invalidateQueries({ queryKey })
34
41
  },
35
42
  [apiClient, conversation_id, queryClient, queryKey, reply_root_id]
36
43
  )
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'
2
2
  import { useAppState } from './use_app_state'
3
3
 
4
4
  type Options = {
5
- fetchNewer: (newestMessageId: string) => void
5
+ fetchNewer: (newestMessageId: string) => Promise<void>
6
6
  newestMessageId?: string
7
7
  enabled: boolean
8
8
  }
@@ -20,6 +20,6 @@ export const useFetchNewerOnForeground = ({ fetchNewer, newestMessageId, enabled
20
20
  wasInBackground.current = false
21
21
  if (!returnedToForeground || !enabled || !newestMessageId) return
22
22
 
23
- fetchNewer(newestMessageId)
23
+ fetchNewer(newestMessageId).catch(() => {})
24
24
  }, [appState, enabled, fetchNewer, newestMessageId])
25
25
  }
@@ -0,0 +1,33 @@
1
+ import { useCallback, useEffect, useRef } from 'react'
2
+
3
+ type Options = {
4
+ fetchNewer: (newestMessageId: string) => Promise<void>
5
+ newestMessageId?: string
6
+ readingHistory: boolean
7
+ caughtUp: boolean
8
+ }
9
+
10
+ export const useHeldBackMessages = ({
11
+ fetchNewer,
12
+ newestMessageId,
13
+ readingHistory,
14
+ caughtUp,
15
+ }: Options) => {
16
+ const heldBackSince = useRef<string | null>(null)
17
+
18
+ const onSkippedNewMessage = useCallback(() => {
19
+ heldBackSince.current ??= newestMessageId ?? null
20
+ }, [newestMessageId])
21
+
22
+ useEffect(() => {
23
+ const since = heldBackSince.current
24
+ if (readingHistory || !caughtUp || !since) return
25
+
26
+ heldBackSince.current = null
27
+ fetchNewer(since).catch(() => {
28
+ heldBackSince.current ??= since
29
+ })
30
+ }, [caughtUp, fetchNewer, readingHistory])
31
+
32
+ return { onSkippedNewMessage }
33
+ }
@@ -1,5 +1,6 @@
1
1
  import { InfiniteData, useMutation } from '@tanstack/react-query'
2
2
  import { chatQueryClient } from '../contexts/api_provider'
3
+ import { useConversationContext } from '../contexts/conversation_context'
3
4
  import { ApiCollection, ApiResource, MessageResource } from '../types'
4
5
  import { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource_for_create'
5
6
  import {
@@ -35,6 +36,12 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
35
36
  const isEditing = !isNewMessage(message)
36
37
  const apiClient = useApiClient()
37
38
  const currentPerson = useCurrentPerson()
39
+ const { messagesQueryKey } = useConversationContext()
40
+ const liveQueryKey = getMessagesQueryKey({
41
+ conversation_id: conversationId,
42
+ reply_root_id: replyRootId,
43
+ })
44
+ const queryKey = isEditing ? (messagesQueryKey ?? liveQueryKey) : liveQueryKey
38
45
 
39
46
  const mutation = useMutation({
40
47
  mutationFn: ({ text, attachments, idempotentKey }: ResolvedMessageMutationVariables) => {
@@ -79,6 +86,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
79
86
  message,
80
87
  text,
81
88
  replyRootId,
89
+ queryKey,
82
90
  })
83
91
 
84
92
  return { message: optimisticMessage }
@@ -102,10 +110,6 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
102
110
 
103
111
  // Add error to the optimistic message from the cache on error
104
112
  if (optimisticMessage) {
105
- const queryKey = getMessagesQueryKey({
106
- conversation_id: conversationId,
107
- reply_root_id: replyRootId,
108
- })
109
113
  chatQueryClient.setQueryData(
110
114
  queryKey,
111
115
  (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>
@@ -125,10 +129,6 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
125
129
  const { message: optimisticMessage } = context || {}
126
130
  const updatedMessage = result.data
127
131
  type QueryData = InfiniteData<ApiCollection<MessageResource>>
128
- const queryKey = getMessagesQueryKey({
129
- conversation_id: conversationId,
130
- reply_root_id: replyRootId,
131
- })
132
132
 
133
133
  // First remove the optimistic message if it exists
134
134
  if (optimisticMessage) {
@@ -1,6 +1,7 @@
1
1
  import { InfiniteData, useMutation, useQueryClient } from '@tanstack/react-query'
2
2
  import { useCallback } from 'react'
3
3
  import { Alert } from 'react-native'
4
+ import { useConversationContext } from '../contexts/conversation_context'
4
5
  import { ApiCollection, ApiResource, MessageResource } from '../types'
5
6
  import { ReactionCountResource } from '../types/resources/reaction'
6
7
  import { updateRecordInPagesData, REACTION_EMOJIS } from '../utils'
@@ -30,7 +31,8 @@ export function useMessageReactionToggle({
30
31
  }: UseMessageReactionToggleProps) {
31
32
  const apiClient = useApiClient()
32
33
  const queryClient = useQueryClient()
33
- const queryKey = getMessagesQueryKey({ conversation_id })
34
+ const { messagesQueryKey } = useConversationContext()
35
+ const queryKey = messagesQueryKey ?? getMessagesQueryKey({ conversation_id })
34
36
  const message_id = message.id
35
37
 
36
38
  const toggleReaction = useCallback(
@@ -45,7 +45,7 @@ export const ServicesForm = ({
45
45
  const customAvatarsEnabled = featureEnabled(availableFeatures.custom_conversation_avatars)
46
46
  const safetyLockEnabled = featureEnabled(availableFeatures.conversation_safety_lock)
47
47
  const [selectedPlanId, setSelectedPlanId] = useState<number | undefined>(initialPlanId)
48
- const [filterByPlan, setFilterByPlan] = useState(!!initialPlanId)
48
+ const [filterByPlan, setFilterByPlan] = useState<boolean>(!!initialPlanId)
49
49
  const initialState = useMemo(() => uniq(initialTeamIds) || [], [initialTeamIds]) // Uniq here because services can send duplicates in the teams_i_lead response.
50
50
  const [selectedTeamIds, setSelectedTeamIds] = useState<number[]>(initialState)
51
51
 
@@ -33,6 +33,7 @@ import { useConversationMessages } from '../hooks/use_conversation_messages'
33
33
  import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
34
34
  import { useFetchNewerMessages } from '../hooks/use_fetch_newer_messages'
35
35
  import { useFetchNewerOnForeground } from '../hooks/use_fetch_newer_on_foreground'
36
+ import { useHeldBackMessages } from '../hooks/use_held_back_messages'
36
37
  import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
37
38
  import {
38
39
  analyticsEvents,
@@ -44,7 +45,9 @@ import { useVideoModerationJoltEvents } from '../hooks/use_video_moderation_jolt
44
45
  import { ConversationResource } from '../types/resources/conversation'
45
46
  import { ConversationBadgeResource } from '../types/resources/conversation_badge'
46
47
  import { MessageResource } from '../types/resources/message'
48
+ import { newestPersistedMessageId } from '../utils/cache/messages_cache'
47
49
  import { gatedMessageCache } from '../utils/gated_message_cache'
50
+ import { getMessagesQueryKey } from '../utils/request/get_messages'
48
51
  import { decodeUlidTime } from '../utils/ulid'
49
52
 
50
53
  export type ConversationRouteProps = {
@@ -85,6 +88,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
85
88
  <ConversationContextProvider
86
89
  conversationId={conversation_id}
87
90
  currentPageReplyRootId={reply_root_id ?? null}
91
+ messagesQueryKey={getMessagesQueryKey({ conversation_id, reply_root_id, anchor })}
88
92
  >
89
93
  {anchor ? (
90
94
  <AnchorNotFoundBoundary onAnchorNotFound={dropAnchor}>
@@ -134,23 +138,27 @@ function ConversationScreenContent({
134
138
  const caughtUp = !hasPreviousPage
135
139
  const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
136
140
  const readingHistory = showJumpToBottomButton
137
- const skippedNewMessages = useRef(false)
138
- const messageCaches = [
139
- gatedMessageCache({
141
+ const newestMessageId = newestPersistedMessageId(messages)
142
+ const fetchNewer = useFetchNewerMessages({ conversation_id, reply_root_id, queryKey })
143
+ const { onSkippedNewMessage } = useHeldBackMessages({
144
+ fetchNewer,
145
+ newestMessageId,
146
+ readingHistory,
147
+ caughtUp,
148
+ })
149
+ useConversationJoltEvents({ conversationId: conversation_id })
150
+ useConversationMessagesJoltEvents({
151
+ conversationId: conversation_id,
152
+ replyRootId: reply_root_id ?? null,
153
+ messageCache: gatedMessageCache({
140
154
  queryKey,
141
155
  acceptNewMessages: caughtUp && !readingHistory,
142
- onSkippedNewMessage: () => {
143
- skippedNewMessages.current = true
144
- },
156
+ onSkippedNewMessage,
145
157
  }),
146
- ]
147
- const newestMessageId = messages[0]?.id
148
- useConversationJoltEvents({ conversationId: conversation_id })
149
- useConversationMessagesJoltEvents({ conversationId: conversation_id, messageCaches })
158
+ })
150
159
  const { markHandledByMessageForm } = useVideoModerationJoltEvents()
151
160
  useEnsureConversationsRouteExists()
152
161
  useMarkLatestMessageRead({ conversation, messages, enabled: !behindLatest })
153
- const fetchNewer = useFetchNewerMessages({ conversation_id, reply_root_id, queryKey })
154
162
  useFetchNewerOnForeground({ fetchNewer, newestMessageId, enabled: anchored && caughtUp })
155
163
  const noMessages = messages.length === 0
156
164
 
@@ -187,9 +195,9 @@ function ConversationScreenContent({
187
195
  }, [handleReturnToBottom, onExitAnchoredMode])
188
196
  const jumpToLatest = useCallback(() => {
189
197
  setShowJumpToBottomButton(false)
190
- if (behindLatest) exitAnchoredMode()
198
+ if (anchored) exitAnchoredMode()
191
199
  else handleReturnToBottom()
192
- }, [behindLatest, exitAnchoredMode, handleReturnToBottom])
200
+ }, [anchored, exitAnchoredMode, handleReturnToBottom])
193
201
 
194
202
  const trackScroll = (event: any) => {
195
203
  const offsetY = event.nativeEvent.contentOffset.y
@@ -203,13 +211,6 @@ function ConversationScreenContent({
203
211
  fetchPreviousPage()
204
212
  }
205
213
 
206
- useEffect(() => {
207
- if (readingHistory || !caughtUp || !newestMessageId || !skippedNewMessages.current) return
208
-
209
- skippedNewMessages.current = false
210
- fetchNewer(newestMessageId)
211
- }, [caughtUp, fetchNewer, newestMessageId, readingHistory])
212
-
213
214
  useEffect(() => {
214
215
  if (reply_root_id) {
215
216
  navigation.setParams({
@@ -9,6 +9,7 @@ import { useReactionStyles } from '../components/conversation/message_reaction'
9
9
  import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
10
10
  import { useCreateAndroidRippleColor, useFontScale, useTheme } from '../hooks'
11
11
  import { useApiClient } from '../hooks/use_api_client'
12
+ import { useConversationMessageLookup } from '../hooks/use_conversation_message_lookup'
12
13
  import { useConversationMessages } from '../hooks/use_conversation_messages'
13
14
  import { availableFeatures, useFeatures } from '../hooks/use_features'
14
15
  import { useMessageReactionToggle } from '../hooks/use_message_reaction_toggle'
@@ -41,11 +42,8 @@ export function MessageActionsScreen({ route }: MessageActionsScreenProps) {
41
42
  isSystemMessage,
42
43
  } = route.params
43
44
 
44
- const { messages, refetch } = useConversationMessages(
45
- { conversation_id },
46
- { refetchOnMount: false }
47
- )
48
- const message = messages.find(m => m.id === message_id)
45
+ const { refetch } = useConversationMessages({ conversation_id }, { refetchOnMount: false })
46
+ const message = useConversationMessageLookup({ conversation_id, message_id })
49
47
 
50
48
  if (!message) return null
51
49
 
@@ -14,7 +14,7 @@ import {
14
14
  } from '../components/display/platform_modal_header_buttons'
15
15
  import BlankState from '../components/primitive/blank_state_primitive'
16
16
  import { useTheme } from '../hooks'
17
- import { useConversationMessages } from '../hooks/use_conversation_messages'
17
+ import { useConversationMessageLookup } from '../hooks/use_conversation_message_lookup'
18
18
  import { useReportMessage } from '../hooks/use_report_message'
19
19
  import { MessageResource } from '../types'
20
20
  import { MessageReportReason } from '../types/resources/message_report'
@@ -51,8 +51,7 @@ export type MessageReportScreenProps = StaticScreenProps<{
51
51
  export function MessageReportScreen({ route }: MessageReportScreenProps) {
52
52
  const { conversation_id, message_id } = route.params
53
53
 
54
- const { messages } = useConversationMessages({ conversation_id }, { refetchOnMount: false })
55
- const message = messages.find(m => m.id === message_id)
54
+ const message = useConversationMessageLookup({ conversation_id, message_id })
56
55
 
57
56
  if (!message) return null
58
57
 
@@ -7,7 +7,7 @@ import { Avatar, Text } from '../components'
7
7
  import { Tabs } from '../components/display/tabs'
8
8
  import FormSheet, { getFormSheetScreenOptions } from '../components/primitive/form_sheet'
9
9
  import { useSuspenseGet, useTheme } from '../hooks'
10
- import { useConversationMessages } from '../hooks/use_conversation_messages'
10
+ import { useConversationMessageLookup } from '../hooks/use_conversation_message_lookup'
11
11
  import { useFontScale } from '../hooks/use_font_scale'
12
12
  import { MemberResource } from '../types'
13
13
  import { ReactionCountResource } from '../types/resources/reaction'
@@ -31,9 +31,7 @@ export function ReactionsScreen({ route }: ReactionScreenProps) {
31
31
  const { conversation_id, message_id, reaction_value } = route.params
32
32
  const styles = useStyles()
33
33
 
34
- const { messages } = useConversationMessages({ conversation_id }, { refetchOnMount: false })
35
-
36
- const message = messages.find(m => m.id === message_id)
34
+ const message = useConversationMessageLookup({ conversation_id, message_id })
37
35
  const reactionCounts = message?.reactionCounts || []
38
36
  const initialReactionCount =
39
37
  reactionCounts.find(r => r.value === reaction_value) || reactionCounts[0]
@@ -0,0 +1,37 @@
1
+ import { QueryClient } from '@tanstack/react-query'
2
+ import { getMessagesQueryKey } from '../../request/get_messages'
3
+ import { findMessageInCaches } from '../find_message_in_caches'
4
+
5
+ const page = (ids: string[]) => ({
6
+ data: ids.map(id => ({ id, type: 'Message' })),
7
+ links: {},
8
+ meta: { count: ids.length, totalCount: ids.length },
9
+ })
10
+
11
+ const seed = (queryClient: QueryClient, key: unknown[], ids: string[]) =>
12
+ queryClient.setQueryData(key, { pages: [page(ids)], pageParams: [{}] })
13
+
14
+ describe('findMessageInCaches', () => {
15
+ const queryClient = new QueryClient()
16
+
17
+ beforeEach(() => {
18
+ queryClient.clear()
19
+ seed(queryClient, getMessagesQueryKey({ conversation_id: 1 }), ['01C', '01B'])
20
+ seed(queryClient, getMessagesQueryKey({ conversation_id: 1, anchor: '01A' }), ['01A', '00Z'])
21
+ seed(queryClient, getMessagesQueryKey({ conversation_id: 1, reply_root_id: '01B' }), ['01R'])
22
+ seed(queryClient, getMessagesQueryKey({ conversation_id: 2 }), ['02A'])
23
+ })
24
+
25
+ it('finds a message in the live cache', () => {
26
+ expect(findMessageInCaches(queryClient, 1, '01B')?.id).toBe('01B')
27
+ })
28
+
29
+ it('finds a message that only the anchored cache holds', () => {
30
+ expect(findMessageInCaches(queryClient, 1, '00Z')?.id).toBe('00Z')
31
+ })
32
+
33
+ it('ignores reply threads and other conversations', () => {
34
+ expect(findMessageInCaches(queryClient, 1, '01R')).toBeUndefined()
35
+ expect(findMessageInCaches(queryClient, 1, '02A')).toBeUndefined()
36
+ })
37
+ })
@@ -1,7 +1,7 @@
1
1
  import { InfiniteData, QueryClient } from '@tanstack/react-query'
2
2
  import type { ApiCollection, MessageResource } from '../../../types'
3
3
  import type { PersonResource } from '../../../types/resources/person'
4
- import { updateCacheWithMessage } from '../messages_cache'
4
+ import { newestPersistedMessageId, updateCacheWithMessage } from '../messages_cache'
5
5
 
6
6
  type MessagesQueryData = InfiniteData<ApiCollection<MessageResource>>
7
7
 
@@ -87,3 +87,16 @@ describe('updateCacheWithMessage on message.created', () => {
87
87
  expect(idsInCache(queryClient)).toEqual(['01F', '01B-temp', '01A-temp'])
88
88
  })
89
89
  })
90
+
91
+ describe('newestPersistedMessageId', () => {
92
+ it('skips optimistic rows so a failed send cannot become the catch-up cursor', () => {
93
+ const messages = [message({ id: '01ZZZ-temp' }), message({ id: '01B' }), message({ id: '01A' })]
94
+
95
+ expect(newestPersistedMessageId(messages)).toBe('01B')
96
+ })
97
+
98
+ it('is undefined when nothing has been persisted', () => {
99
+ expect(newestPersistedMessageId([message({ id: '01ZZZ-temp' })])).toBeUndefined()
100
+ expect(newestPersistedMessageId([])).toBeUndefined()
101
+ })
102
+ })
@@ -5,31 +5,30 @@ const page = (ids: string[], next?: { idGt: string }) => ({
5
5
  links: {},
6
6
  meta: { count: ids.length, totalCount: ids.length, next },
7
7
  })
8
- const pageParam = { where: { id_gt: '01C' }, order: 'asc' }
9
8
 
10
9
  describe('prependNewerPage', () => {
11
10
  const cached = { pages: [page(['01C', '01B']), page(['01A'])], pageParams: [{}, { offset: 1 }] }
12
11
 
13
- it('prepends only the messages the cache does not already hold', () => {
14
- const result = prependNewerPage(cached, page(['01C', '01D', '01E']), pageParam)
12
+ it('merges the new rows into the first page and leaves the page params alone', () => {
13
+ const result = prependNewerPage(cached, [page(['01C', '01D', '01E'])])
15
14
 
16
- expect(result?.pages[0].data.map(m => m.id)).toEqual(['01D', '01E'])
17
- expect(result?.pages).toHaveLength(3)
18
- expect(result?.pageParams[0]).toEqual(pageParam)
15
+ expect(result?.pages).toHaveLength(2)
16
+ expect(result?.pages[0].data.map(m => m.id)).toEqual(['01D', '01E', '01C', '01B'])
17
+ expect(result?.pageParams).toEqual(cached.pageParams)
19
18
  })
20
19
 
21
- it('leaves the cache alone when nothing new came back', () => {
22
- expect(prependNewerPage(cached, page(['01C']), pageParam)).toBe(cached)
23
- })
20
+ it('merges every fetched batch, newest last', () => {
21
+ const result = prependNewerPage(cached, [page(['01D', '01E']), page(['01F'])])
24
22
 
25
- it('still prepends an empty page when the server reports more newer messages', () => {
26
- const result = prependNewerPage(cached, page(['01C'], { idGt: '01C' }), pageParam)
23
+ expect(result?.pages[0].data.map(m => m.id)).toEqual(['01D', '01E', '01F', '01C', '01B'])
24
+ })
27
25
 
28
- expect(result?.pages[0].data).toEqual([])
29
- expect(result?.pages[0].meta.next).toEqual({ idGt: '01C' })
26
+ it('returns the same data when nothing new came back', () => {
27
+ expect(prependNewerPage(cached, [page(['01C'])])).toBe(cached)
28
+ expect(prependNewerPage(cached, [])).toBe(cached)
30
29
  })
31
30
 
32
31
  it('does nothing without cached data', () => {
33
- expect(prependNewerPage(undefined, page(['01D']), pageParam)).toBeUndefined()
32
+ expect(prependNewerPage(undefined, [page(['01D'])])).toBeUndefined()
34
33
  })
35
34
  })
@@ -0,0 +1,20 @@
1
+ import { InfiniteData, QueryClient } from '@tanstack/react-query'
2
+ import { ApiCollection, MessageResource } from '../../types'
3
+ import { getMessagesRequestArgs } from '../request/get_messages'
4
+
5
+ type QueryData = InfiniteData<ApiCollection<MessageResource>>
6
+
7
+ export const findMessageInCaches = (
8
+ queryClient: QueryClient,
9
+ conversation_id: number,
10
+ message_id: string
11
+ ): MessageResource | undefined => {
12
+ const { url } = getMessagesRequestArgs({ conversation_id })
13
+
14
+ for (const [, data] of queryClient.getQueriesData<QueryData>({ queryKey: [url] })) {
15
+ const found = data?.pages.flatMap(page => page.data).find(message => message.id === message_id)
16
+ if (found) return found
17
+ }
18
+
19
+ return undefined
20
+ }
@@ -135,6 +135,9 @@ type MessagesQueryData = InfiniteData<ApiCollection<MessageResource>>
135
135
  export function isTemporaryMessageId(messageId?: string | null): boolean {
136
136
  return !!messageId && messageId.endsWith('-temp')
137
137
  }
138
+ export function newestPersistedMessageId(messages: MessageResource[]): string | undefined {
139
+ return messages.find(message => !isTemporaryMessageId(message.id))?.id
140
+ }
138
141
  export function isNewMessage(message?: MessageResource): boolean {
139
142
  return !message?.id || isTemporaryMessageId(message.id)
140
143
  }
@@ -9,19 +9,16 @@ export function optimisticallyUpdateMessage({
9
9
  message,
10
10
  text,
11
11
  replyRootId,
12
+ queryKey = getMessagesQueryKey({ conversation_id: conversationId, reply_root_id: replyRootId }),
12
13
  }: {
13
14
  conversationId: number
14
15
  message: MessageResource
15
16
  text: string
16
17
  replyRootId?: string | null
18
+ queryKey?: unknown[]
17
19
  }) {
18
20
  type QueryData = InfiniteData<ApiCollection<MessageResource>>
19
21
 
20
- const queryKey = getMessagesQueryKey({
21
- conversation_id: conversationId,
22
- reply_root_id: replyRootId,
23
- })
24
-
25
22
  let optimisticMessage = message
26
23
 
27
24
  chatQueryClient.setQueryData<QueryData>(queryKey, data =>
@@ -6,17 +6,14 @@ type PagesData<T extends ResourceObject> = InfiniteData<ApiCollection<T>, Partia
6
6
 
7
7
  export function prependNewerPage<T extends ResourceObject>(
8
8
  data: PagesData<T> | undefined,
9
- page: ApiCollection<T>,
10
- pageParam: Partial<RequestData>
9
+ pages: ApiCollection<T>[]
11
10
  ): PagesData<T> | undefined {
12
- if (!data) return data
11
+ if (!data || data.pages.length === 0) return data
13
12
 
14
- const knownIds = new Set(data.pages.flatMap(p => p.data.map(record => record.id)))
15
- const fresh = page.data.filter(record => !knownIds.has(record.id))
16
- if (fresh.length === 0 && !page.meta?.next) return data
13
+ const knownIds = new Set(data.pages.flatMap(page => page.data.map(record => record.id)))
14
+ const fresh = pages.flatMap(page => page.data).filter(record => !knownIds.has(record.id))
15
+ if (fresh.length === 0) return data
17
16
 
18
- return {
19
- pages: [{ ...page, data: fresh }, ...data.pages],
20
- pageParams: [pageParam, ...data.pageParams],
21
- }
17
+ const [first, ...rest] = data.pages
18
+ return { ...data, pages: [{ ...first, data: [...fresh, ...first.data] }, ...rest] }
22
19
  }