@planningcenter/chat-react-native 4.2.0-rc.5 → 4.2.1-qa-staging.0
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_form.d.ts +2 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +54 -51
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversation/message_list.d.ts +2 -2
- package/build/components/conversation/message_list.d.ts.map +1 -1
- package/build/components/conversation/message_list.js +19 -16
- package/build/components/conversation/message_list.js.map +1 -1
- package/build/hooks/index.d.ts +1 -0
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +1 -0
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/paginator_meta.d.ts +3 -1
- package/build/hooks/paginator_meta.d.ts.map +1 -1
- package/build/hooks/paginator_meta.js +25 -8
- package/build/hooks/paginator_meta.js.map +1 -1
- package/build/hooks/use_api.d.ts.map +1 -1
- package/build/hooks/use_api.js +5 -8
- package/build/hooks/use_api.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts +9 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +88 -73
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_direct_message_eligibility.d.ts +14 -0
- package/build/hooks/use_direct_message_eligibility.d.ts.map +1 -0
- package/build/hooks/use_direct_message_eligibility.js +87 -0
- package/build/hooks/use_direct_message_eligibility.js.map +1 -0
- package/build/hooks/use_fetch_newer_messages.d.ts +8 -0
- package/build/hooks/use_fetch_newer_messages.d.ts.map +1 -0
- package/build/hooks/use_fetch_newer_messages.js +56 -0
- package/build/hooks/use_fetch_newer_messages.js.map +1 -0
- package/build/hooks/use_suspense_api.d.ts.map +1 -1
- package/build/hooks/use_suspense_api.js +5 -8
- package/build/hooks/use_suspense_api.js.map +1 -1
- package/build/screens/conversation_new/components/services_form.js +9 -8
- package/build/screens/conversation_new/components/services_form.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +137 -322
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/types/api_primitives.d.ts +2 -0
- package/build/types/api_primitives.d.ts.map +1 -1
- package/build/types/api_primitives.js.map +1 -1
- package/build/types/resources/member.d.ts +1 -0
- package/build/types/resources/member.d.ts.map +1 -1
- package/build/types/resources/member.js.map +1 -1
- package/build/utils/cache/prepend_newer_page.d.ts +7 -0
- package/build/utils/cache/prepend_newer_page.d.ts.map +1 -0
- package/build/utils/cache/prepend_newer_page.js +14 -0
- package/build/utils/cache/prepend_newer_page.js.map +1 -0
- package/build/utils/client/types.d.ts +1 -0
- package/build/utils/client/types.d.ts.map +1 -1
- package/build/utils/client/types.js.map +1 -1
- package/build/utils/gated_message_cache.d.ts +7 -0
- package/build/utils/gated_message_cache.d.ts.map +1 -0
- package/build/utils/gated_message_cache.js +13 -0
- package/build/utils/gated_message_cache.js.map +1 -0
- package/build/utils/group_messages.js +4 -1
- package/build/utils/group_messages.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/hooks/paginator_meta.test.ts +73 -2
- package/src/__tests__/hooks/use_conversation_messages_jolt_events.test.ts +36 -0
- package/src/__tests__/hooks/use_direct_message_eligibility.test.tsx +83 -0
- package/src/__tests__/screens/conversation_new/components/services_form.test.tsx +91 -0
- package/src/__tests__/utils/gated_message_cache.test.ts +26 -0
- package/src/components/conversation/message_form.tsx +4 -0
- package/src/components/conversation/message_list.tsx +8 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/paginator_meta.ts +29 -5
- package/src/hooks/use_api.ts +4 -5
- package/src/hooks/use_conversation_messages_jolt_events.ts +57 -40
- package/src/hooks/use_direct_message_eligibility.ts +40 -0
- package/src/hooks/use_fetch_newer_messages.ts +37 -0
- package/src/hooks/use_suspense_api.ts +4 -5
- package/src/screens/conversation_new/components/services_form.tsx +5 -5
- package/src/screens/conversation_screen.tsx +36 -4
- package/src/types/api_primitives.ts +2 -0
- package/src/types/resources/member.ts +1 -0
- package/src/utils/__tests__/group_messages.test.ts +29 -0
- package/src/utils/cache/__tests__/prepend_newer_page.test.ts +35 -0
- package/src/utils/cache/prepend_newer_page.ts +22 -0
- package/src/utils/client/types.ts +1 -0
- package/src/utils/gated_message_cache.ts +19 -0
- package/src/utils/group_messages.ts +4 -1
|
@@ -30,6 +30,7 @@ import { useConversation } from '../hooks/use_conversation'
|
|
|
30
30
|
import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
|
|
31
31
|
import { useConversationMessages } from '../hooks/use_conversation_messages'
|
|
32
32
|
import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
|
|
33
|
+
import { useFetchNewerMessages } from '../hooks/use_fetch_newer_messages'
|
|
33
34
|
import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
|
|
34
35
|
import {
|
|
35
36
|
analyticsEvents,
|
|
@@ -41,6 +42,7 @@ import { useVideoModerationJoltEvents } from '../hooks/use_video_moderation_jolt
|
|
|
41
42
|
import { ConversationResource } from '../types/resources/conversation'
|
|
42
43
|
import { ConversationBadgeResource } from '../types/resources/conversation_badge'
|
|
43
44
|
import { MessageResource } from '../types/resources/message'
|
|
45
|
+
import { gatedMessageCache } from '../utils/gated_message_cache'
|
|
44
46
|
|
|
45
47
|
export type ConversationRouteProps = {
|
|
46
48
|
conversation_id: number
|
|
@@ -84,15 +86,29 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
84
86
|
const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
|
|
85
87
|
route.params
|
|
86
88
|
const { data: conversation } = useConversation(route.params)
|
|
87
|
-
const { messages, fetchNextPage, hasNextPage } = useConversationMessages({
|
|
89
|
+
const { messages, fetchNextPage, hasNextPage, queryKey } = useConversationMessages({
|
|
88
90
|
conversation_id,
|
|
89
91
|
reply_root_id,
|
|
90
92
|
})
|
|
93
|
+
const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
|
|
94
|
+
const readingHistory = showJumpToBottomButton
|
|
95
|
+
const skippedNewMessages = useRef(false)
|
|
96
|
+
const messageCaches = [
|
|
97
|
+
gatedMessageCache({
|
|
98
|
+
queryKey,
|
|
99
|
+
acceptNewMessages: !readingHistory,
|
|
100
|
+
onSkippedNewMessage: () => {
|
|
101
|
+
skippedNewMessages.current = true
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
]
|
|
105
|
+
const newestMessageId = messages[0]?.id
|
|
91
106
|
useConversationJoltEvents({ conversationId: conversation_id })
|
|
92
|
-
useConversationMessagesJoltEvents({ conversationId: conversation_id })
|
|
107
|
+
useConversationMessagesJoltEvents({ conversationId: conversation_id, messageCaches })
|
|
93
108
|
const { markHandledByMessageForm } = useVideoModerationJoltEvents()
|
|
94
109
|
useEnsureConversationsRouteExists()
|
|
95
110
|
useMarkLatestMessageRead({ conversation, messages })
|
|
111
|
+
const fetchNewer = useFetchNewerMessages({ conversation_id, reply_root_id, queryKey })
|
|
96
112
|
const noMessages = messages.length === 0
|
|
97
113
|
|
|
98
114
|
const { repliesDisabled, memberAbility, badges, title } = conversation
|
|
@@ -107,7 +123,6 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
107
123
|
const muted = conversation.conversationMembership?.muted ?? conversation.muted
|
|
108
124
|
|
|
109
125
|
const listRef = useRef<FlatList>(null)
|
|
110
|
-
const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
|
|
111
126
|
|
|
112
127
|
const trackScroll = (event: any) => {
|
|
113
128
|
const offsetY = event.nativeEvent.contentOffset.y
|
|
@@ -126,6 +141,18 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
126
141
|
})
|
|
127
142
|
}, [restOffset])
|
|
128
143
|
|
|
144
|
+
const jumpToLatest = useCallback(() => {
|
|
145
|
+
setShowJumpToBottomButton(false)
|
|
146
|
+
handleReturnToBottom()
|
|
147
|
+
}, [handleReturnToBottom])
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (readingHistory || !newestMessageId || !skippedNewMessages.current) return
|
|
151
|
+
|
|
152
|
+
skippedNewMessages.current = false
|
|
153
|
+
fetchNewer(newestMessageId)
|
|
154
|
+
}, [fetchNewer, newestMessageId, readingHistory])
|
|
155
|
+
|
|
129
156
|
useEffect(() => {
|
|
130
157
|
if (reply_root_id) {
|
|
131
158
|
navigation.setParams({
|
|
@@ -181,9 +208,10 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
181
208
|
onContentSizeChange={onContentSizeChange}
|
|
182
209
|
onScrollEndDrag={onScrollEndDrag}
|
|
183
210
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
211
|
+
maintainVisibleContentPosition={readingHistory ? { minIndexForVisible: 0 } : undefined}
|
|
184
212
|
/>
|
|
185
213
|
)}
|
|
186
|
-
<JumpToBottomButton onPress={
|
|
214
|
+
<JumpToBottomButton onPress={jumpToLatest} visible={showJumpToBottomButton} />
|
|
187
215
|
{!noMessages && <TypingIndicator />}
|
|
188
216
|
{showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
|
|
189
217
|
<ConversationBottomBar
|
|
@@ -193,6 +221,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
193
221
|
replyRootId={reply_root_id}
|
|
194
222
|
currentlyEditingMessage={currentlyEditingMessage}
|
|
195
223
|
onFlaggedInMessageForm={markHandledByMessageForm}
|
|
224
|
+
onBeforeSubmit={jumpToLatest}
|
|
196
225
|
/>
|
|
197
226
|
</KeyboardView>
|
|
198
227
|
</View>
|
|
@@ -206,6 +235,7 @@ interface ConversationBottomBarProps {
|
|
|
206
235
|
replyRootId: string | null | undefined
|
|
207
236
|
currentlyEditingMessage: MessageResource | undefined
|
|
208
237
|
onFlaggedInMessageForm?: (id: string) => void
|
|
238
|
+
onBeforeSubmit?: () => void
|
|
209
239
|
}
|
|
210
240
|
|
|
211
241
|
function ConversationBottomBar({
|
|
@@ -215,6 +245,7 @@ function ConversationBottomBar({
|
|
|
215
245
|
replyRootId,
|
|
216
246
|
currentlyEditingMessage,
|
|
217
247
|
onFlaggedInMessageForm,
|
|
248
|
+
onBeforeSubmit,
|
|
218
249
|
}: ConversationBottomBarProps) {
|
|
219
250
|
const messageLink = useMessageAvailableMembersAction(conversation)
|
|
220
251
|
|
|
@@ -233,6 +264,7 @@ function ConversationBottomBar({
|
|
|
233
264
|
replyRootId={replyRootId}
|
|
234
265
|
currentlyEditingMessage={currentlyEditingMessage}
|
|
235
266
|
onFlaggedInMessageForm={onFlaggedInMessageForm}
|
|
267
|
+
onBeforeSubmit={onBeforeSubmit}
|
|
236
268
|
key={
|
|
237
269
|
currentlyEditingMessage
|
|
238
270
|
? `edit-message-form-${currentlyEditingMessage.id}`
|
|
@@ -33,12 +33,14 @@ export interface NextPageCursor {
|
|
|
33
33
|
idLte?: string
|
|
34
34
|
idGt?: string
|
|
35
35
|
idGte?: string
|
|
36
|
+
order?: 'asc' | 'desc'
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface CollectionMeta {
|
|
39
40
|
count: number
|
|
40
41
|
totalCount: number
|
|
41
42
|
next?: NextPageCursor
|
|
43
|
+
prev?: NextPageCursor
|
|
42
44
|
parent?: ResourceObject
|
|
43
45
|
[attributeName: string]: unknown
|
|
44
46
|
}
|
|
@@ -170,6 +170,35 @@ describe('groupMessages — reply roots', () => {
|
|
|
170
170
|
|
|
171
171
|
expect(findShadow(enriched, '02-rootA')).toMatchObject({ replyRoot: null })
|
|
172
172
|
})
|
|
173
|
+
|
|
174
|
+
it.each([
|
|
175
|
+
['is still saving', { pending: true }],
|
|
176
|
+
['failed to save', { error: 'Failed to send message' }],
|
|
177
|
+
])('keeps the saved snapshot while a root edit %s', (_label, unsavedState) => {
|
|
178
|
+
const snapshot = message('rootA', { replyRootId: 'rootA', text: 'saved' })
|
|
179
|
+
const messages = [
|
|
180
|
+
message('03', { replyRootId: 'rootA', replyRoot: snapshot }),
|
|
181
|
+
message('02'),
|
|
182
|
+
message('rootA', { replyRootId: 'rootA', text: 'unsaved', ...unsavedState }),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
const enriched = groupMessages({ ms: messages })
|
|
186
|
+
|
|
187
|
+
expect(findShadow(enriched, '03-rootA')).toMatchObject({ replyRoot: { text: 'saved' } })
|
|
188
|
+
expect((findEnriched(enriched, 'rootA') as MessageResource).text).toBe('unsaved')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('shows the unsaved root edit when the reply carries no snapshot to fall back on', () => {
|
|
192
|
+
const messages = [
|
|
193
|
+
message('03', { replyRootId: 'rootA' }),
|
|
194
|
+
message('02'),
|
|
195
|
+
message('rootA', { replyRootId: 'rootA', text: 'unsaved', pending: true }),
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
const enriched = groupMessages({ ms: messages })
|
|
199
|
+
|
|
200
|
+
expect(findShadow(enriched, '03-rootA')).toMatchObject({ replyRoot: { text: 'unsaved' } })
|
|
201
|
+
})
|
|
173
202
|
})
|
|
174
203
|
|
|
175
204
|
describe('groupMessages — system messages', () => {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { prependNewerPage } from '../prepend_newer_page'
|
|
2
|
+
|
|
3
|
+
const page = (ids: string[], next?: { idGt: string }) => ({
|
|
4
|
+
data: ids.map(id => ({ id, type: 'Message' })),
|
|
5
|
+
links: {},
|
|
6
|
+
meta: { count: ids.length, totalCount: ids.length, next },
|
|
7
|
+
})
|
|
8
|
+
const pageParam = { where: { id_gt: '01C' }, order: 'asc' }
|
|
9
|
+
|
|
10
|
+
describe('prependNewerPage', () => {
|
|
11
|
+
const cached = { pages: [page(['01C', '01B']), page(['01A'])], pageParams: [{}, { offset: 1 }] }
|
|
12
|
+
|
|
13
|
+
it('prepends only the messages the cache does not already hold', () => {
|
|
14
|
+
const result = prependNewerPage(cached, page(['01C', '01D', '01E']), pageParam)
|
|
15
|
+
|
|
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)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('leaves the cache alone when nothing new came back', () => {
|
|
22
|
+
expect(prependNewerPage(cached, page(['01C']), pageParam)).toBe(cached)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('still prepends an empty page when the server reports more newer messages', () => {
|
|
26
|
+
const result = prependNewerPage(cached, page(['01C'], { idGt: '01C' }), pageParam)
|
|
27
|
+
|
|
28
|
+
expect(result?.pages[0].data).toEqual([])
|
|
29
|
+
expect(result?.pages[0].meta.next).toEqual({ idGt: '01C' })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('does nothing without cached data', () => {
|
|
33
|
+
expect(prependNewerPage(undefined, page(['01D']), pageParam)).toBeUndefined()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { InfiniteData } from '@tanstack/react-query'
|
|
2
|
+
import { ApiCollection, ResourceObject } from '../../types'
|
|
3
|
+
import { RequestData } from '../client'
|
|
4
|
+
|
|
5
|
+
type PagesData<T extends ResourceObject> = InfiniteData<ApiCollection<T>, Partial<RequestData>>
|
|
6
|
+
|
|
7
|
+
export function prependNewerPage<T extends ResourceObject>(
|
|
8
|
+
data: PagesData<T> | undefined,
|
|
9
|
+
page: ApiCollection<T>,
|
|
10
|
+
pageParam: Partial<RequestData>
|
|
11
|
+
): PagesData<T> | undefined {
|
|
12
|
+
if (!data) return data
|
|
13
|
+
|
|
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
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
pages: [{ ...page, data: fresh }, ...data.pages],
|
|
20
|
+
pageParams: [pageParam, ...data.pageParams],
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MessagesCache } from '../hooks/use_conversation_messages_jolt_events'
|
|
2
|
+
|
|
3
|
+
export const gatedMessageCache = ({
|
|
4
|
+
queryKey,
|
|
5
|
+
acceptNewMessages,
|
|
6
|
+
onSkippedNewMessage,
|
|
7
|
+
}: {
|
|
8
|
+
queryKey: unknown[]
|
|
9
|
+
acceptNewMessages: boolean
|
|
10
|
+
onSkippedNewMessage?: () => void
|
|
11
|
+
}): MessagesCache => ({
|
|
12
|
+
queryKey,
|
|
13
|
+
canUpdate: event => {
|
|
14
|
+
if (event !== 'message.created' || acceptNewMessages) return true
|
|
15
|
+
|
|
16
|
+
onSkippedNewMessage?.()
|
|
17
|
+
return false
|
|
18
|
+
},
|
|
19
|
+
})
|
|
@@ -74,7 +74,10 @@ function resolveReplyRoot(
|
|
|
74
74
|
messagesById: Map<string, MessageResource>
|
|
75
75
|
): MessageResource | null {
|
|
76
76
|
if (!message.replyRootId || message.replyRootId === message.id) return null
|
|
77
|
-
|
|
77
|
+
const loadedRoot = messagesById.get(message.replyRootId)
|
|
78
|
+
const loadedRootIsSaved = !!loadedRoot && !loadedRoot.pending && !loadedRoot.error
|
|
79
|
+
if (loadedRootIsSaved) return loadedRoot
|
|
80
|
+
return message.replyRoot ?? loadedRoot ?? null
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
function enrichSystemMessage(
|