@planningcenter/chat-react-native 4.2.0 → 4.2.1-qa-staging.1
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/anchor_not_found_boundary.d.ts +18 -0
- package/build/components/conversation/anchor_not_found_boundary.d.ts.map +1 -0
- package/build/components/conversation/anchor_not_found_boundary.js +20 -0
- package/build/components/conversation/anchor_not_found_boundary.js.map +1 -0
- 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 +3 -2
- package/build/components/conversation/message_list.d.ts.map +1 -1
- package/build/components/conversation/message_list.js +152 -66
- 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.d.ts +8 -2
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +77 -45
- package/build/hooks/use_conversation_messages.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_fetch_newer_on_foreground.d.ts +8 -0
- package/build/hooks/use_fetch_newer_on_foreground.d.ts.map +1 -0
- package/build/hooks/use_fetch_newer_on_foreground.js +41 -0
- package/build/hooks/use_fetch_newer_on_foreground.js.map +1 -0
- package/build/hooks/use_mark_latest_message_read.d.ts +2 -1
- package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
- package/build/hooks/use_mark_latest_message_read.js +4 -3
- package/build/hooks/use_mark_latest_message_read.js.map +1 -1
- 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 +1 -0
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +241 -355
- 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/generate_placeholder_ulid.d.ts +0 -6
- package/build/utils/generate_placeholder_ulid.d.ts.map +1 -1
- package/build/utils/generate_placeholder_ulid.js +2 -2
- package/build/utils/generate_placeholder_ulid.js.map +1 -1
- package/build/utils/group_messages.js +4 -1
- package/build/utils/group_messages.js.map +1 -1
- package/build/utils/request/get_messages.d.ts +18 -6
- package/build/utils/request/get_messages.d.ts.map +1 -1
- package/build/utils/request/get_messages.js +10 -12
- package/build/utils/request/get_messages.js.map +1 -1
- package/build/utils/ulid.d.ts +3 -0
- package/build/utils/ulid.d.ts.map +1 -0
- package/build/utils/ulid.js +7 -0
- package/build/utils/ulid.js.map +1 -0
- 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/__tests__/utils/request/get_messages.test.ts +24 -0
- package/src/__tests__/utils/ulid.test.ts +15 -0
- package/src/components/conversation/__tests__/anchor_not_found_boundary.test.tsx +51 -0
- package/src/components/conversation/__tests__/message_list.test.tsx +42 -0
- package/src/components/conversation/anchor_not_found_boundary.tsx +21 -0
- package/src/components/conversation/message_form.tsx +4 -0
- package/src/components/conversation/message_list.tsx +58 -4
- 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.ts +43 -11
- 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_fetch_newer_on_foreground.ts +25 -0
- package/src/hooks/use_mark_latest_message_read.ts +4 -3
- 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 +120 -17
- 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/generate_placeholder_ulid.ts +2 -2
- package/src/utils/group_messages.ts +4 -1
- package/src/utils/request/get_messages.ts +12 -14
- package/src/utils/ulid.ts +11 -0
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
useTheme as useNavigationTheme,
|
|
8
8
|
useRoute,
|
|
9
9
|
} from '@react-navigation/native'
|
|
10
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
10
|
+
import React, { startTransition, useCallback, useEffect, useRef, useState } from 'react'
|
|
11
11
|
import { FlatList, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
12
12
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
13
13
|
import { Badge, Icon } from '../components'
|
|
14
|
+
import { AnchorNotFoundBoundary } from '../components/conversation/anchor_not_found_boundary'
|
|
14
15
|
import { ConversationStartBanner } from '../components/conversation/conversation_start_banner'
|
|
15
16
|
import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
|
|
16
17
|
import { MessageForm } from '../components/conversation/message_form'
|
|
@@ -30,6 +31,8 @@ import { useConversation } from '../hooks/use_conversation'
|
|
|
30
31
|
import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
|
|
31
32
|
import { useConversationMessages } from '../hooks/use_conversation_messages'
|
|
32
33
|
import { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'
|
|
34
|
+
import { useFetchNewerMessages } from '../hooks/use_fetch_newer_messages'
|
|
35
|
+
import { useFetchNewerOnForeground } from '../hooks/use_fetch_newer_on_foreground'
|
|
33
36
|
import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
|
|
34
37
|
import {
|
|
35
38
|
analyticsEvents,
|
|
@@ -41,6 +44,8 @@ import { useVideoModerationJoltEvents } from '../hooks/use_video_moderation_jolt
|
|
|
41
44
|
import { ConversationResource } from '../types/resources/conversation'
|
|
42
45
|
import { ConversationBadgeResource } from '../types/resources/conversation_badge'
|
|
43
46
|
import { MessageResource } from '../types/resources/message'
|
|
47
|
+
import { gatedMessageCache } from '../utils/gated_message_cache'
|
|
48
|
+
import { decodeUlidTime } from '../utils/ulid'
|
|
44
49
|
|
|
45
50
|
export type ConversationRouteProps = {
|
|
46
51
|
conversation_id: number
|
|
@@ -49,6 +54,7 @@ export type ConversationRouteProps = {
|
|
|
49
54
|
chat_group_graph_id?: string
|
|
50
55
|
clear_input?: boolean
|
|
51
56
|
editing_message_id?: number | null
|
|
57
|
+
initial_message_id?: string | null
|
|
52
58
|
title?: string
|
|
53
59
|
subtitle?: string
|
|
54
60
|
badge?: ConversationBadgeResource
|
|
@@ -59,9 +65,16 @@ export type ConversationRouteProps = {
|
|
|
59
65
|
export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
|
|
60
66
|
|
|
61
67
|
export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
62
|
-
const { conversation_id, reply_root_id } = route.params
|
|
68
|
+
const { conversation_id, reply_root_id, initial_message_id } = route.params
|
|
63
69
|
|
|
64
70
|
const { data: conversation } = useConversation({ conversation_id })
|
|
71
|
+
const [anchor, setAnchor] = useState(() =>
|
|
72
|
+
isBeforeLatestMessage(initial_message_id, conversation?.lastMessageCreatedAt)
|
|
73
|
+
? initial_message_id
|
|
74
|
+
: null
|
|
75
|
+
)
|
|
76
|
+
const exitAnchoredMode = useCallback(() => startTransition(() => setAnchor(null)), [])
|
|
77
|
+
const dropAnchor = useCallback(() => setAnchor(null), [])
|
|
65
78
|
|
|
66
79
|
usePublishProductAnalyticsEvent(analyticsEvents.conversation_show_opened, {
|
|
67
80
|
reply_root_id,
|
|
@@ -73,26 +86,72 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
|
73
86
|
conversationId={conversation_id}
|
|
74
87
|
currentPageReplyRootId={reply_root_id ?? null}
|
|
75
88
|
>
|
|
76
|
-
|
|
89
|
+
{anchor ? (
|
|
90
|
+
<AnchorNotFoundBoundary onAnchorNotFound={dropAnchor}>
|
|
91
|
+
<ConversationScreenContent
|
|
92
|
+
route={route}
|
|
93
|
+
anchor={anchor}
|
|
94
|
+
onExitAnchoredMode={exitAnchoredMode}
|
|
95
|
+
/>
|
|
96
|
+
</AnchorNotFoundBoundary>
|
|
97
|
+
) : (
|
|
98
|
+
<ConversationScreenContent
|
|
99
|
+
route={route}
|
|
100
|
+
anchor={null}
|
|
101
|
+
onExitAnchoredMode={exitAnchoredMode}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
77
104
|
</ConversationContextProvider>
|
|
78
105
|
)
|
|
79
106
|
}
|
|
80
107
|
|
|
81
|
-
|
|
108
|
+
interface ConversationScreenContentProps extends ConversationScreenProps {
|
|
109
|
+
anchor: string | null | undefined
|
|
110
|
+
onExitAnchoredMode: () => void
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ConversationScreenContent({
|
|
114
|
+
route,
|
|
115
|
+
anchor,
|
|
116
|
+
onExitAnchoredMode,
|
|
117
|
+
}: ConversationScreenContentProps) {
|
|
82
118
|
const styles = useStyles()
|
|
83
119
|
const navigation = useNavigation()
|
|
84
120
|
const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
|
|
85
121
|
route.params
|
|
86
122
|
const { data: conversation } = useConversation(route.params)
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
123
|
+
const anchored = !!anchor
|
|
124
|
+
const {
|
|
125
|
+
messages,
|
|
126
|
+
fetchNextPage,
|
|
127
|
+
hasNextPage,
|
|
128
|
+
fetchPreviousPage,
|
|
129
|
+
hasPreviousPage,
|
|
130
|
+
isFetchingPreviousPage,
|
|
131
|
+
queryKey,
|
|
132
|
+
} = useConversationMessages({ conversation_id, reply_root_id, anchor })
|
|
133
|
+
const behindLatest = anchored && hasPreviousPage
|
|
134
|
+
const caughtUp = !hasPreviousPage
|
|
135
|
+
const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
|
|
136
|
+
const readingHistory = showJumpToBottomButton
|
|
137
|
+
const skippedNewMessages = useRef(false)
|
|
138
|
+
const messageCaches = [
|
|
139
|
+
gatedMessageCache({
|
|
140
|
+
queryKey,
|
|
141
|
+
acceptNewMessages: caughtUp && !readingHistory,
|
|
142
|
+
onSkippedNewMessage: () => {
|
|
143
|
+
skippedNewMessages.current = true
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
]
|
|
147
|
+
const newestMessageId = messages[0]?.id
|
|
91
148
|
useConversationJoltEvents({ conversationId: conversation_id })
|
|
92
|
-
useConversationMessagesJoltEvents({ conversationId: conversation_id })
|
|
149
|
+
useConversationMessagesJoltEvents({ conversationId: conversation_id, messageCaches })
|
|
93
150
|
const { markHandledByMessageForm } = useVideoModerationJoltEvents()
|
|
94
151
|
useEnsureConversationsRouteExists()
|
|
95
|
-
useMarkLatestMessageRead({ conversation, messages })
|
|
152
|
+
useMarkLatestMessageRead({ conversation, messages, enabled: !behindLatest })
|
|
153
|
+
const fetchNewer = useFetchNewerMessages({ conversation_id, reply_root_id, queryKey })
|
|
154
|
+
useFetchNewerOnForeground({ fetchNewer, newestMessageId, enabled: anchored && caughtUp })
|
|
96
155
|
const noMessages = messages.length === 0
|
|
97
156
|
|
|
98
157
|
const { repliesDisabled, memberAbility, badges, title } = conversation
|
|
@@ -107,12 +166,8 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
107
166
|
const muted = conversation.conversationMembership?.muted ?? conversation.muted
|
|
108
167
|
|
|
109
168
|
const listRef = useRef<FlatList>(null)
|
|
110
|
-
const [
|
|
111
|
-
|
|
112
|
-
const trackScroll = (event: any) => {
|
|
113
|
-
const offsetY = event.nativeEvent.contentOffset.y
|
|
114
|
-
setShowJumpToBottomButton(offsetY > 200)
|
|
115
|
-
}
|
|
169
|
+
const [hasDragged, setHasDragged] = useState(false)
|
|
170
|
+
const holdListPosition = behindLatest || readingHistory
|
|
116
171
|
|
|
117
172
|
const { restOffset, onLayout, onContentSizeChange, onMomentumScrollEnd, onScrollEndDrag } =
|
|
118
173
|
useRestAwayFromDismissEdge({
|
|
@@ -126,6 +181,35 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
126
181
|
})
|
|
127
182
|
}, [restOffset])
|
|
128
183
|
|
|
184
|
+
const exitAnchoredMode = useCallback(() => {
|
|
185
|
+
onExitAnchoredMode()
|
|
186
|
+
handleReturnToBottom()
|
|
187
|
+
}, [handleReturnToBottom, onExitAnchoredMode])
|
|
188
|
+
const jumpToLatest = useCallback(() => {
|
|
189
|
+
setShowJumpToBottomButton(false)
|
|
190
|
+
if (behindLatest) exitAnchoredMode()
|
|
191
|
+
else handleReturnToBottom()
|
|
192
|
+
}, [behindLatest, exitAnchoredMode, handleReturnToBottom])
|
|
193
|
+
|
|
194
|
+
const trackScroll = (event: any) => {
|
|
195
|
+
const offsetY = event.nativeEvent.contentOffset.y
|
|
196
|
+
setShowJumpToBottomButton(offsetY > 200)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const loadNewerMessages = () => {
|
|
200
|
+
if (!hasPreviousPage || isFetchingPreviousPage) return
|
|
201
|
+
|
|
202
|
+
setHasDragged(false)
|
|
203
|
+
fetchPreviousPage()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (readingHistory || !caughtUp || !newestMessageId || !skippedNewMessages.current) return
|
|
208
|
+
|
|
209
|
+
skippedNewMessages.current = false
|
|
210
|
+
fetchNewer(newestMessageId)
|
|
211
|
+
}, [caughtUp, fetchNewer, newestMessageId, readingHistory])
|
|
212
|
+
|
|
129
213
|
useEffect(() => {
|
|
130
214
|
if (reply_root_id) {
|
|
131
215
|
navigation.setParams({
|
|
@@ -174,6 +258,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
174
258
|
conversationId={conversation_id}
|
|
175
259
|
messages={messages}
|
|
176
260
|
replyRootId={reply_root_id}
|
|
261
|
+
anchorMessageId={anchor}
|
|
177
262
|
hasNextPage={hasNextPage}
|
|
178
263
|
onEndReached={() => fetchNextPage()}
|
|
179
264
|
onScroll={trackScroll}
|
|
@@ -181,9 +266,18 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
181
266
|
onContentSizeChange={onContentSizeChange}
|
|
182
267
|
onScrollEndDrag={onScrollEndDrag}
|
|
183
268
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
269
|
+
onScrollBeginDrag={anchored ? () => setHasDragged(true) : undefined}
|
|
270
|
+
onStartReached={anchored && hasDragged ? loadNewerMessages : undefined}
|
|
271
|
+
onStartReachedThreshold={anchored ? 0.5 : undefined}
|
|
272
|
+
maintainVisibleContentPosition={
|
|
273
|
+
holdListPosition ? { minIndexForVisible: 0 } : undefined
|
|
274
|
+
}
|
|
184
275
|
/>
|
|
185
276
|
)}
|
|
186
|
-
<JumpToBottomButton
|
|
277
|
+
<JumpToBottomButton
|
|
278
|
+
onPress={jumpToLatest}
|
|
279
|
+
visible={behindLatest || showJumpToBottomButton}
|
|
280
|
+
/>
|
|
187
281
|
{!noMessages && <TypingIndicator />}
|
|
188
282
|
{showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
|
|
189
283
|
<ConversationBottomBar
|
|
@@ -193,6 +287,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
193
287
|
replyRootId={reply_root_id}
|
|
194
288
|
currentlyEditingMessage={currentlyEditingMessage}
|
|
195
289
|
onFlaggedInMessageForm={markHandledByMessageForm}
|
|
290
|
+
onBeforeSubmit={jumpToLatest}
|
|
196
291
|
/>
|
|
197
292
|
</KeyboardView>
|
|
198
293
|
</View>
|
|
@@ -206,6 +301,7 @@ interface ConversationBottomBarProps {
|
|
|
206
301
|
replyRootId: string | null | undefined
|
|
207
302
|
currentlyEditingMessage: MessageResource | undefined
|
|
208
303
|
onFlaggedInMessageForm?: (id: string) => void
|
|
304
|
+
onBeforeSubmit?: () => void
|
|
209
305
|
}
|
|
210
306
|
|
|
211
307
|
function ConversationBottomBar({
|
|
@@ -215,6 +311,7 @@ function ConversationBottomBar({
|
|
|
215
311
|
replyRootId,
|
|
216
312
|
currentlyEditingMessage,
|
|
217
313
|
onFlaggedInMessageForm,
|
|
314
|
+
onBeforeSubmit,
|
|
218
315
|
}: ConversationBottomBarProps) {
|
|
219
316
|
const messageLink = useMessageAvailableMembersAction(conversation)
|
|
220
317
|
|
|
@@ -233,6 +330,7 @@ function ConversationBottomBar({
|
|
|
233
330
|
replyRootId={replyRootId}
|
|
234
331
|
currentlyEditingMessage={currentlyEditingMessage}
|
|
235
332
|
onFlaggedInMessageForm={onFlaggedInMessageForm}
|
|
333
|
+
onBeforeSubmit={onBeforeSubmit}
|
|
236
334
|
key={
|
|
237
335
|
currentlyEditingMessage
|
|
238
336
|
? `edit-message-form-${currentlyEditingMessage.id}`
|
|
@@ -247,6 +345,11 @@ function ConversationBottomBar({
|
|
|
247
345
|
)
|
|
248
346
|
}
|
|
249
347
|
|
|
348
|
+
const isBeforeLatestMessage = (anchor?: string | null, latestMessageCreatedAt?: string) =>
|
|
349
|
+
!!anchor &&
|
|
350
|
+
!!latestMessageCreatedAt &&
|
|
351
|
+
decodeUlidTime(anchor) < Date.parse(latestMessageCreatedAt)
|
|
352
|
+
|
|
250
353
|
interface ConversationScreenTitleProps extends HeaderTitleProps {
|
|
251
354
|
conversation_id: number
|
|
252
355
|
badge?: ConversationBadgeResource
|
|
@@ -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
|
+
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ULID_CHARS } from './ulid'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The actual `ulid` library depends on a secure random number generator, which looks like it
|
|
3
5
|
* may require a native dependency to polyfill in React Native.
|
|
@@ -5,8 +7,6 @@
|
|
|
5
7
|
* implementation that doesn't require any native dependencies.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
const ULID_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' // Crockford's Base32
|
|
9
|
-
|
|
10
10
|
function encodeBase32(value: number, length: number) {
|
|
11
11
|
let result = ''
|
|
12
12
|
for (let i = 0; i < length; i++) {
|
|
@@ -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(
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { getRequestQueryKey } from '../../hooks/use_suspense_api'
|
|
2
2
|
import { getMessageFields, getMessagesInclude } from './messages_data_options'
|
|
3
3
|
|
|
4
|
+
type MessagesRequestOptions = {
|
|
5
|
+
conversation_id: number
|
|
6
|
+
reply_root_id?: string | null
|
|
7
|
+
anchor?: string | null
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
export const getMessagesRequestArgs = ({
|
|
5
11
|
conversation_id,
|
|
6
12
|
reply_root_id,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
reply_root_id?: string | null
|
|
10
|
-
}) => {
|
|
13
|
+
anchor,
|
|
14
|
+
}: MessagesRequestOptions) => {
|
|
11
15
|
const normalizedReplyRootId = reply_root_id ?? null
|
|
12
16
|
const url = normalizedReplyRootId
|
|
13
17
|
? `/me/conversations/${conversation_id}/messages/${normalizedReplyRootId}/replies`
|
|
14
18
|
: `/me/conversations/${conversation_id}/messages`
|
|
19
|
+
const anchored = anchor ? { where: { id_around: anchor } } : {}
|
|
15
20
|
|
|
16
21
|
return {
|
|
17
22
|
url,
|
|
@@ -19,18 +24,11 @@ export const getMessagesRequestArgs = ({
|
|
|
19
24
|
perPage: 25,
|
|
20
25
|
fields: getMessageFields,
|
|
21
26
|
include: getMessagesInclude,
|
|
27
|
+
...anchored,
|
|
22
28
|
},
|
|
23
29
|
reply_root_id: normalizedReplyRootId,
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
export const getMessagesQueryKey = (
|
|
28
|
-
|
|
29
|
-
reply_root_id,
|
|
30
|
-
}: {
|
|
31
|
-
conversation_id: number
|
|
32
|
-
reply_root_id?: string | null
|
|
33
|
-
}) => {
|
|
34
|
-
const requestArgs = getMessagesRequestArgs({ conversation_id, reply_root_id })
|
|
35
|
-
return getRequestQueryKey(requestArgs)
|
|
36
|
-
}
|
|
33
|
+
export const getMessagesQueryKey = (options: MessagesRequestOptions) =>
|
|
34
|
+
getRequestQueryKey(getMessagesRequestArgs(options))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const ULID_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' // Crockford's Base32
|
|
2
|
+
|
|
3
|
+
const TIME_LENGTH = 10
|
|
4
|
+
|
|
5
|
+
export function decodeUlidTime(ulid: string): number {
|
|
6
|
+
return ulid
|
|
7
|
+
.slice(0, TIME_LENGTH)
|
|
8
|
+
.toUpperCase()
|
|
9
|
+
.split('')
|
|
10
|
+
.reduce((time, char) => time * 32 + ULID_CHARS.indexOf(char), 0)
|
|
11
|
+
}
|