@planningcenter/chat-react-native 3.18.0-rc.6 → 3.18.0-rc.8

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 (60) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +23 -12
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/message_form.d.ts.map +1 -1
  5. package/build/components/conversation/message_form.js +2 -4
  6. package/build/components/conversation/message_form.js.map +1 -1
  7. package/build/components/conversation/typing_indicator.d.ts +1 -5
  8. package/build/components/conversation/typing_indicator.d.ts.map +1 -1
  9. package/build/components/conversation/typing_indicator.js +2 -2
  10. package/build/components/conversation/typing_indicator.js.map +1 -1
  11. package/build/contexts/conversation_context.d.ts +13 -0
  12. package/build/contexts/conversation_context.d.ts.map +1 -0
  13. package/build/contexts/conversation_context.js +14 -0
  14. package/build/contexts/conversation_context.js.map +1 -0
  15. package/build/hooks/use_broadcast_typing_status.d.ts +1 -1
  16. package/build/hooks/use_broadcast_typing_status.d.ts.map +1 -1
  17. package/build/hooks/use_broadcast_typing_status.js +7 -3
  18. package/build/hooks/use_broadcast_typing_status.js.map +1 -1
  19. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  20. package/build/hooks/use_conversation_messages_jolt_events.js +23 -70
  21. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  22. package/build/hooks/use_message_create_or_update.d.ts +0 -2
  23. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  24. package/build/hooks/use_message_create_or_update.js +10 -8
  25. package/build/hooks/use_message_create_or_update.js.map +1 -1
  26. package/build/hooks/use_typing_indicators.d.ts +1 -1
  27. package/build/hooks/use_typing_indicators.d.ts.map +1 -1
  28. package/build/hooks/use_typing_indicators.js +16 -3
  29. package/build/hooks/use_typing_indicators.js.map +1 -1
  30. package/build/screens/conversation_screen.d.ts.map +1 -1
  31. package/build/screens/conversation_screen.js +8 -1
  32. package/build/screens/conversation_screen.js.map +1 -1
  33. package/build/types/jolt_events/reaction_events.d.ts +1 -0
  34. package/build/types/jolt_events/reaction_events.d.ts.map +1 -1
  35. package/build/types/jolt_events/reaction_events.js.map +1 -1
  36. package/build/types/jolt_events/typing_events.d.ts +1 -0
  37. package/build/types/jolt_events/typing_events.d.ts.map +1 -1
  38. package/build/types/jolt_events/typing_events.js.map +1 -1
  39. package/build/utils/cache/messages_cache.d.ts +9 -0
  40. package/build/utils/cache/messages_cache.d.ts.map +1 -0
  41. package/build/utils/cache/messages_cache.js +89 -0
  42. package/build/utils/cache/messages_cache.js.map +1 -0
  43. package/build/utils/cache/optimistically_create_message.d.ts +2 -1
  44. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  45. package/build/utils/cache/optimistically_create_message.js +6 -3
  46. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/components/conversation/message.tsx +34 -16
  49. package/src/components/conversation/message_form.tsx +2 -9
  50. package/src/components/conversation/typing_indicator.tsx +2 -6
  51. package/src/contexts/conversation_context.tsx +34 -0
  52. package/src/hooks/use_broadcast_typing_status.ts +7 -3
  53. package/src/hooks/use_conversation_messages_jolt_events.ts +39 -81
  54. package/src/hooks/use_message_create_or_update.ts +10 -9
  55. package/src/hooks/use_typing_indicators.ts +15 -3
  56. package/src/screens/conversation_screen.tsx +15 -1
  57. package/src/types/jolt_events/reaction_events.ts +1 -0
  58. package/src/types/jolt_events/typing_events.ts +1 -0
  59. package/src/utils/cache/messages_cache.ts +113 -0
  60. package/src/utils/cache/optimistically_create_message.ts +7 -2
@@ -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.
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
2
2
  import { useMemo } from 'react'
3
3
  import { PersonTyping } from './use_typing_status_cache'
4
4
  import { useCurrentPerson } from './use_current_person'
5
+ import { useConversationContext } from '../contexts/conversation_context'
5
6
 
6
7
  /**
7
8
  * Hook for getting people currently typing in a conversation
@@ -9,9 +10,12 @@ import { useCurrentPerson } from './use_current_person'
9
10
  * The query function itself doesn't do anything, but we add to the cache
10
11
  * from useTypingStatusCache when we receive a typing event.
11
12
  */
12
- export const useTypingIndicators = (conversationId: number) => {
13
+ export const useTypingIndicators = () => {
14
+ const { conversationId, currentPageReplyRootId } = useConversationContext()
13
15
  const cacheKey = useMemo(() => ['conversationTyping', String(conversationId)], [conversationId])
14
16
  const currentPerson = useCurrentPerson()
17
+ const isCurrentPerson = (authorId: number) => authorId === currentPerson?.id
18
+ const inMainConversationView = !currentPageReplyRootId
15
19
  const now = Date.now()
16
20
  const { data } = useQuery({
17
21
  queryKey: cacheKey,
@@ -19,8 +23,16 @@ export const useTypingIndicators = (conversationId: number) => {
19
23
  initialData: stableArray,
20
24
  })
21
25
 
22
- // Filter out expired entries and the current user
23
- return data.filter(person => person.expires > now && person.author_id !== currentPerson?.id)
26
+ return data.filter(({ author_id, expires, reply_root_id }) => {
27
+ if (isCurrentPerson(author_id)) return false
28
+ if (now > expires) return false
29
+
30
+ // If you are in the main conversation view, you will see any message sent
31
+ if (inMainConversationView) return true
32
+
33
+ // If you are in a reply view, you will only see messages sent in this reply thread
34
+ return reply_root_id === currentPageReplyRootId
35
+ })
24
36
  }
25
37
 
26
38
  /**
@@ -36,6 +36,7 @@ import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events
36
36
  import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
37
37
  import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
38
38
  import { availableFeatures, useFeatures } from '../hooks/use_features'
39
+ import { ConversationContextProvider } from '../contexts/conversation_context'
39
40
 
40
41
  export type ConversationRouteProps = {
41
42
  conversation_id: number
@@ -53,6 +54,19 @@ export type ConversationRouteProps = {
53
54
  export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
54
55
 
55
56
  export function ConversationScreen({ route }: ConversationScreenProps) {
57
+ const { conversation_id, reply_root_id } = route.params
58
+
59
+ return (
60
+ <ConversationContextProvider
61
+ conversationId={conversation_id}
62
+ currentPageReplyRootId={reply_root_id ?? null}
63
+ >
64
+ <ConversationScreenContent route={route} />
65
+ </ConversationContextProvider>
66
+ )
67
+ }
68
+
69
+ function ConversationScreenContent({ route }: ConversationScreenProps) {
56
70
  const styles = useStyles()
57
71
  const navigation = useNavigation()
58
72
  const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
@@ -179,7 +193,7 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
179
193
  />
180
194
  )}
181
195
  <JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
182
- {!noMessages && <TypingIndicator conversationId={conversation_id} />}
196
+ {!noMessages && <TypingIndicator />}
183
197
  {showLeaderDisabledReplyBanner && <LeaderDisabledRepliesBanner />}
184
198
  {canReply ? (
185
199
  <MessageForm.Root
@@ -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']
@@ -13,4 +13,5 @@ export interface TypingBroadcastDataAttributes {
13
13
  author_id: number
14
14
  author_name: string
15
15
  id: string
16
+ reply_root_id: string | null
16
17
  }
@@ -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({