@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.
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +23 -12
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +2 -4
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversation/typing_indicator.d.ts +1 -5
- package/build/components/conversation/typing_indicator.d.ts.map +1 -1
- package/build/components/conversation/typing_indicator.js +2 -2
- package/build/components/conversation/typing_indicator.js.map +1 -1
- package/build/contexts/conversation_context.d.ts +13 -0
- package/build/contexts/conversation_context.d.ts.map +1 -0
- package/build/contexts/conversation_context.js +14 -0
- package/build/contexts/conversation_context.js.map +1 -0
- package/build/hooks/use_broadcast_typing_status.d.ts +1 -1
- package/build/hooks/use_broadcast_typing_status.d.ts.map +1 -1
- package/build/hooks/use_broadcast_typing_status.js +7 -3
- package/build/hooks/use_broadcast_typing_status.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +23 -70
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_message_create_or_update.d.ts +0 -2
- package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
- package/build/hooks/use_message_create_or_update.js +10 -8
- package/build/hooks/use_message_create_or_update.js.map +1 -1
- package/build/hooks/use_typing_indicators.d.ts +1 -1
- package/build/hooks/use_typing_indicators.d.ts.map +1 -1
- package/build/hooks/use_typing_indicators.js +16 -3
- package/build/hooks/use_typing_indicators.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +8 -1
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/types/jolt_events/reaction_events.d.ts +1 -0
- package/build/types/jolt_events/reaction_events.d.ts.map +1 -1
- package/build/types/jolt_events/reaction_events.js.map +1 -1
- package/build/types/jolt_events/typing_events.d.ts +1 -0
- package/build/types/jolt_events/typing_events.d.ts.map +1 -1
- package/build/types/jolt_events/typing_events.js.map +1 -1
- package/build/utils/cache/messages_cache.d.ts +9 -0
- package/build/utils/cache/messages_cache.d.ts.map +1 -0
- package/build/utils/cache/messages_cache.js +89 -0
- package/build/utils/cache/messages_cache.js.map +1 -0
- package/build/utils/cache/optimistically_create_message.d.ts +2 -1
- package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
- package/build/utils/cache/optimistically_create_message.js +6 -3
- package/build/utils/cache/optimistically_create_message.js.map +1 -1
- package/package.json +2 -2
- package/src/components/conversation/message.tsx +34 -16
- package/src/components/conversation/message_form.tsx +2 -9
- package/src/components/conversation/typing_indicator.tsx +2 -6
- package/src/contexts/conversation_context.tsx +34 -0
- package/src/hooks/use_broadcast_typing_status.ts +7 -3
- package/src/hooks/use_conversation_messages_jolt_events.ts +39 -81
- package/src/hooks/use_message_create_or_update.ts +10 -9
- package/src/hooks/use_typing_indicators.ts +15 -3
- package/src/screens/conversation_screen.tsx +15 -1
- package/src/types/jolt_events/reaction_events.ts +1 -0
- package/src/types/jolt_events/typing_events.ts +1 -0
- package/src/utils/cache/messages_cache.ts +113 -0
- 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({
|
|
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({
|
|
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 = (
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
196
|
+
{!noMessages && <TypingIndicator />}
|
|
183
197
|
{showLeaderDisabledReplyBanner && <LeaderDisabledRepliesBanner />}
|
|
184
198
|
{canReply ? (
|
|
185
199
|
<MessageForm.Root
|
|
@@ -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({
|
|
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({
|