@planningcenter/chat-react-native 4.2.1-qa-staging.0 → 4.2.1-qa-staging.2

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 (50) hide show
  1. package/build/components/conversation/anchor_not_found_boundary.d.ts +18 -0
  2. package/build/components/conversation/anchor_not_found_boundary.d.ts.map +1 -0
  3. package/build/components/conversation/anchor_not_found_boundary.js +20 -0
  4. package/build/components/conversation/anchor_not_found_boundary.js.map +1 -0
  5. package/build/components/conversation/message_list.d.ts +3 -2
  6. package/build/components/conversation/message_list.d.ts.map +1 -1
  7. package/build/components/conversation/message_list.js +149 -66
  8. package/build/components/conversation/message_list.js.map +1 -1
  9. package/build/hooks/use_conversation_messages.d.ts +8 -2
  10. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  11. package/build/hooks/use_conversation_messages.js +77 -45
  12. package/build/hooks/use_conversation_messages.js.map +1 -1
  13. package/build/hooks/use_fetch_newer_on_foreground.d.ts +8 -0
  14. package/build/hooks/use_fetch_newer_on_foreground.d.ts.map +1 -0
  15. package/build/hooks/use_fetch_newer_on_foreground.js +41 -0
  16. package/build/hooks/use_fetch_newer_on_foreground.js.map +1 -0
  17. package/build/hooks/use_mark_latest_message_read.d.ts +2 -1
  18. package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
  19. package/build/hooks/use_mark_latest_message_read.js +4 -3
  20. package/build/hooks/use_mark_latest_message_read.js.map +1 -1
  21. package/build/screens/conversation_screen.d.ts +1 -0
  22. package/build/screens/conversation_screen.d.ts.map +1 -1
  23. package/build/screens/conversation_screen.js +119 -48
  24. package/build/screens/conversation_screen.js.map +1 -1
  25. package/build/utils/generate_placeholder_ulid.d.ts +0 -6
  26. package/build/utils/generate_placeholder_ulid.d.ts.map +1 -1
  27. package/build/utils/generate_placeholder_ulid.js +2 -2
  28. package/build/utils/generate_placeholder_ulid.js.map +1 -1
  29. package/build/utils/request/get_messages.d.ts +18 -6
  30. package/build/utils/request/get_messages.d.ts.map +1 -1
  31. package/build/utils/request/get_messages.js +10 -12
  32. package/build/utils/request/get_messages.js.map +1 -1
  33. package/build/utils/ulid.d.ts +3 -0
  34. package/build/utils/ulid.d.ts.map +1 -0
  35. package/build/utils/ulid.js +7 -0
  36. package/build/utils/ulid.js.map +1 -0
  37. package/package.json +3 -3
  38. package/src/__tests__/utils/request/get_messages.test.ts +24 -0
  39. package/src/__tests__/utils/ulid.test.ts +15 -0
  40. package/src/components/conversation/__tests__/anchor_not_found_boundary.test.tsx +51 -0
  41. package/src/components/conversation/__tests__/message_list.test.tsx +42 -0
  42. package/src/components/conversation/anchor_not_found_boundary.tsx +21 -0
  43. package/src/components/conversation/message_list.tsx +50 -3
  44. package/src/hooks/use_conversation_messages.ts +43 -11
  45. package/src/hooks/use_fetch_newer_on_foreground.ts +25 -0
  46. package/src/hooks/use_mark_latest_message_read.ts +4 -3
  47. package/src/screens/conversation_screen.tsx +92 -21
  48. package/src/utils/generate_placeholder_ulid.ts +2 -2
  49. package/src/utils/request/get_messages.ts +12 -14
  50. package/src/utils/ulid.ts +11 -0
@@ -1,5 +1,6 @@
1
1
  import { date as formatDate } from '@planningcenter/datetime-fmt'
2
- import React, { RefObject } from 'react'
2
+ import { noop } from 'lodash'
3
+ import React, { RefObject, useRef } from 'react'
3
4
  import { FlatList, FlatListProps, StyleSheet, View } from 'react-native'
4
5
  import { useTheme } from '../../hooks'
5
6
  import { ConversationResource } from '../../types/resources/conversation'
@@ -21,6 +22,9 @@ type ScrollHandlers = Pick<
21
22
  | 'onContentSizeChange'
22
23
  | 'onScrollEndDrag'
23
24
  | 'onMomentumScrollEnd'
25
+ | 'onScrollBeginDrag'
26
+ | 'onStartReached'
27
+ | 'onStartReachedThreshold'
24
28
  | 'maintainVisibleContentPosition'
25
29
  >
26
30
 
@@ -30,6 +34,7 @@ interface MessageListProps extends ScrollHandlers {
30
34
  conversationId: number
31
35
  messages: MessageResource[]
32
36
  replyRootId?: string | null
37
+ anchorMessageId?: string | null
33
38
  hasNextPage: boolean
34
39
  onEndReached: () => void
35
40
  }
@@ -43,6 +48,7 @@ export function MessageList({
43
48
  conversationId,
44
49
  messages,
45
50
  replyRootId,
51
+ anchorMessageId,
46
52
  hasNextPage,
47
53
  onEndReached,
48
54
  onScroll,
@@ -50,6 +56,9 @@ export function MessageList({
50
56
  onContentSizeChange,
51
57
  onScrollEndDrag,
52
58
  onMomentumScrollEnd,
59
+ onScrollBeginDrag,
60
+ onStartReached,
61
+ onStartReachedThreshold,
53
62
  maintainVisibleContentPosition,
54
63
  }: MessageListProps) {
55
64
  const styles = useStyles()
@@ -58,6 +67,40 @@ export function MessageList({
58
67
  conversation.memberAbility?.canDeleteNonAuthoredMessages ?? false
59
68
  const latestRead = conversation.latestReadMessageSortKey
60
69
  const messagesWithSeparators = groupMessages({ ms: messages, inReplyScreen })
70
+ const anchorIndex = anchorMessageId
71
+ ? messagesWithSeparators.findIndex(item => item.id === anchorMessageId)
72
+ : -1
73
+ const layoutHeight = useRef(0)
74
+ const contentHeight = useRef(0)
75
+ const userHasScrolled = useRef(false)
76
+
77
+ const pinAnchorToBottomEdge = () => {
78
+ if (anchorIndex < 0 || userHasScrolled.current) return
79
+ const scrollable = layoutHeight.current > 0 && contentHeight.current > layoutHeight.current
80
+ if (!scrollable) return
81
+
82
+ listRef.current?.scrollToIndex({ index: anchorIndex, viewPosition: 0, animated: false })
83
+ }
84
+
85
+ const handleLayout: FlatListProps<EnrichedMessage>['onLayout'] = event => {
86
+ onLayout?.(event)
87
+ layoutHeight.current = event.nativeEvent.layout.height
88
+ pinAnchorToBottomEdge()
89
+ }
90
+
91
+ const handleContentSizeChange: FlatListProps<EnrichedMessage>['onContentSizeChange'] = (
92
+ width,
93
+ height
94
+ ) => {
95
+ onContentSizeChange?.(width, height)
96
+ contentHeight.current = height
97
+ pinAnchorToBottomEdge()
98
+ }
99
+
100
+ const handleScrollBeginDrag: FlatListProps<EnrichedMessage>['onScrollBeginDrag'] = event => {
101
+ userHasScrolled.current = true
102
+ onScrollBeginDrag?.(event)
103
+ }
61
104
 
62
105
  return (
63
106
  <FlatList
@@ -67,8 +110,9 @@ export function MessageList({
67
110
  data={messagesWithSeparators}
68
111
  keyExtractor={item => item.id}
69
112
  onScroll={onScroll}
70
- onLayout={onLayout}
71
- onContentSizeChange={onContentSizeChange}
113
+ onLayout={handleLayout}
114
+ onContentSizeChange={handleContentSizeChange}
115
+ onScrollToIndexFailed={noop}
72
116
  onScrollEndDrag={onScrollEndDrag}
73
117
  onMomentumScrollEnd={onMomentumScrollEnd}
74
118
  scrollEventThrottle={10}
@@ -108,6 +152,9 @@ export function MessageList({
108
152
  )
109
153
  }}
110
154
  onEndReached={onEndReached}
155
+ onScrollBeginDrag={handleScrollBeginDrag}
156
+ onStartReached={onStartReached}
157
+ onStartReachedThreshold={onStartReachedThreshold}
111
158
  maintainVisibleContentPosition={maintainVisibleContentPosition}
112
159
  ListHeaderComponent={<View style={styles.listHeader} />}
113
160
  />
@@ -2,21 +2,43 @@ import { useMemo } from 'react'
2
2
  import { MessageResource } from '../types'
3
3
  import { replaceEqualById } from '../utils/replace_equal_by_id'
4
4
  import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
5
+ import { getNewerPageParam } from './paginator_meta'
5
6
  import { SuspensePaginatorOptions, useSuspensePaginator } from './use_suspense_api'
6
7
 
8
+ type Options = {
9
+ conversation_id: number
10
+ reply_root_id?: string | null
11
+ anchor?: string | null
12
+ }
13
+
7
14
  export const useConversationMessages = (
8
- { conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string | null },
15
+ { conversation_id, reply_root_id, anchor }: Options,
9
16
  opts?: SuspensePaginatorOptions
10
17
  ) => {
11
- const { data, refetch, isRefetching, fetchNextPage, hasNextPage } =
12
- useSuspensePaginator<MessageResource>(
13
- getMessagesRequestArgs({ conversation_id, reply_root_id }),
14
- {
15
- structuralSharing: replaceEqualById,
16
- ...opts,
17
- }
18
- )
19
- const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id })
18
+ const {
19
+ data,
20
+ refetch,
21
+ isRefetching,
22
+ fetchNextPage,
23
+ hasNextPage,
24
+ fetchPreviousPage,
25
+ hasPreviousPage,
26
+ isFetchingPreviousPage,
27
+ } = useSuspensePaginator<MessageResource>(
28
+ getMessagesRequestArgs({ conversation_id, reply_root_id, anchor }),
29
+ {
30
+ structuralSharing: replaceEqualById,
31
+ ...(anchor
32
+ ? {
33
+ getPreviousPageParam: (page, _pages, pageParam) => getNewerPageParam(page, pageParam),
34
+ gcTime: 0,
35
+ staleTime: Infinity,
36
+ }
37
+ : {}),
38
+ ...opts,
39
+ }
40
+ )
41
+ const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id, anchor })
20
42
  const messages = useMemo(
21
43
  () =>
22
44
  data
@@ -29,5 +51,15 @@ export const useConversationMessages = (
29
51
  [data]
30
52
  )
31
53
 
32
- return { messages, refetch, isRefetching, fetchNextPage, hasNextPage, queryKey }
54
+ return {
55
+ messages,
56
+ refetch,
57
+ isRefetching,
58
+ fetchNextPage,
59
+ hasNextPage,
60
+ fetchPreviousPage,
61
+ hasPreviousPage,
62
+ isFetchingPreviousPage,
63
+ queryKey,
64
+ }
33
65
  }
@@ -0,0 +1,25 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { useAppState } from './use_app_state'
3
+
4
+ type Options = {
5
+ fetchNewer: (newestMessageId: string) => void
6
+ newestMessageId?: string
7
+ enabled: boolean
8
+ }
9
+
10
+ export const useFetchNewerOnForeground = ({ fetchNewer, newestMessageId, enabled }: Options) => {
11
+ const appState = useAppState()
12
+ const wasInBackground = useRef(false)
13
+
14
+ useEffect(() => {
15
+ if (appState !== 'active') {
16
+ wasInBackground.current = true
17
+ return
18
+ }
19
+ const returnedToForeground = wasInBackground.current
20
+ wasInBackground.current = false
21
+ if (!returnedToForeground || !enabled || !newestMessageId) return
22
+
23
+ fetchNewer(newestMessageId)
24
+ }, [appState, enabled, fetchNewer, newestMessageId])
25
+ }
@@ -7,9 +7,10 @@ import { useConversationsMarkRead } from './use_conversations_actions'
7
7
  interface Props {
8
8
  conversation: ConversationResource
9
9
  messages: MessageResource[]
10
+ enabled?: boolean
10
11
  }
11
12
 
12
- export function useMarkLatestMessageRead({ conversation }: Props) {
13
+ export function useMarkLatestMessageRead({ conversation, enabled = true }: Props) {
13
14
  const firedOnce = useRef<boolean>(false)
14
15
  const { markRead } = useConversationsMarkRead({ conversation })
15
16
  const debouncedMarkRead = useMemo(
@@ -24,10 +25,10 @@ export function useMarkLatestMessageRead({ conversation }: Props) {
24
25
  const isActive = appState === 'active'
25
26
 
26
27
  useEffect(() => {
27
- if (!isActive || !shouldMarkRead) return
28
+ if (!enabled || !isActive || !shouldMarkRead) return
28
29
 
29
30
  firedOnce.current = true
30
31
 
31
32
  debouncedMarkRead(true)
32
- }, [debouncedMarkRead, isActive, shouldMarkRead, unreadReaction])
33
+ }, [debouncedMarkRead, enabled, isActive, shouldMarkRead, unreadReaction])
33
34
  }
@@ -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'
@@ -31,6 +32,7 @@ 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'
33
34
  import { useFetchNewerMessages } from '../hooks/use_fetch_newer_messages'
35
+ import { useFetchNewerOnForeground } from '../hooks/use_fetch_newer_on_foreground'
34
36
  import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'
35
37
  import {
36
38
  analyticsEvents,
@@ -43,6 +45,7 @@ import { ConversationResource } from '../types/resources/conversation'
43
45
  import { ConversationBadgeResource } from '../types/resources/conversation_badge'
44
46
  import { MessageResource } from '../types/resources/message'
45
47
  import { gatedMessageCache } from '../utils/gated_message_cache'
48
+ import { decodeUlidTime } from '../utils/ulid'
46
49
 
47
50
  export type ConversationRouteProps = {
48
51
  conversation_id: number
@@ -51,6 +54,7 @@ export type ConversationRouteProps = {
51
54
  chat_group_graph_id?: string
52
55
  clear_input?: boolean
53
56
  editing_message_id?: number | null
57
+ initial_message_id?: string | null
54
58
  title?: string
55
59
  subtitle?: string
56
60
  badge?: ConversationBadgeResource
@@ -61,9 +65,16 @@ export type ConversationRouteProps = {
61
65
  export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
62
66
 
63
67
  export function ConversationScreen({ route }: ConversationScreenProps) {
64
- const { conversation_id, reply_root_id } = route.params
68
+ const { conversation_id, reply_root_id, initial_message_id } = route.params
65
69
 
66
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), [])
67
78
 
68
79
  usePublishProductAnalyticsEvent(analyticsEvents.conversation_show_opened, {
69
80
  reply_root_id,
@@ -75,28 +86,59 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
75
86
  conversationId={conversation_id}
76
87
  currentPageReplyRootId={reply_root_id ?? null}
77
88
  >
78
- <ConversationScreenContent route={route} />
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
+ )}
79
104
  </ConversationContextProvider>
80
105
  )
81
106
  }
82
107
 
83
- function ConversationScreenContent({ route }: ConversationScreenProps) {
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) {
84
118
  const styles = useStyles()
85
119
  const navigation = useNavigation()
86
120
  const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
87
121
  route.params
88
122
  const { data: conversation } = useConversation(route.params)
89
- const { messages, fetchNextPage, hasNextPage, queryKey } = useConversationMessages({
90
- conversation_id,
91
- reply_root_id,
92
- })
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
93
135
  const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)
94
136
  const readingHistory = showJumpToBottomButton
95
137
  const skippedNewMessages = useRef(false)
96
138
  const messageCaches = [
97
139
  gatedMessageCache({
98
140
  queryKey,
99
- acceptNewMessages: !readingHistory,
141
+ acceptNewMessages: caughtUp && !readingHistory,
100
142
  onSkippedNewMessage: () => {
101
143
  skippedNewMessages.current = true
102
144
  },
@@ -107,8 +149,9 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
107
149
  useConversationMessagesJoltEvents({ conversationId: conversation_id, messageCaches })
108
150
  const { markHandledByMessageForm } = useVideoModerationJoltEvents()
109
151
  useEnsureConversationsRouteExists()
110
- useMarkLatestMessageRead({ conversation, messages })
152
+ useMarkLatestMessageRead({ conversation, messages, enabled: !behindLatest })
111
153
  const fetchNewer = useFetchNewerMessages({ conversation_id, reply_root_id, queryKey })
154
+ useFetchNewerOnForeground({ fetchNewer, newestMessageId, enabled: anchored && caughtUp })
112
155
  const noMessages = messages.length === 0
113
156
 
114
157
  const { repliesDisabled, memberAbility, badges, title } = conversation
@@ -123,11 +166,8 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
123
166
  const muted = conversation.conversationMembership?.muted ?? conversation.muted
124
167
 
125
168
  const listRef = useRef<FlatList>(null)
126
-
127
- const trackScroll = (event: any) => {
128
- const offsetY = event.nativeEvent.contentOffset.y
129
- setShowJumpToBottomButton(offsetY > 200)
130
- }
169
+ const [hasDragged, setHasDragged] = useState(false)
170
+ const holdListPosition = behindLatest || readingHistory
131
171
 
132
172
  const { restOffset, onLayout, onContentSizeChange, onMomentumScrollEnd, onScrollEndDrag } =
133
173
  useRestAwayFromDismissEdge({
@@ -141,17 +181,34 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
141
181
  })
142
182
  }, [restOffset])
143
183
 
184
+ const exitAnchoredMode = useCallback(() => {
185
+ onExitAnchoredMode()
186
+ handleReturnToBottom()
187
+ }, [handleReturnToBottom, onExitAnchoredMode])
144
188
  const jumpToLatest = useCallback(() => {
145
189
  setShowJumpToBottomButton(false)
146
- handleReturnToBottom()
147
- }, [handleReturnToBottom])
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
+ }
148
205
 
149
206
  useEffect(() => {
150
- if (readingHistory || !newestMessageId || !skippedNewMessages.current) return
207
+ if (readingHistory || !caughtUp || !newestMessageId || !skippedNewMessages.current) return
151
208
 
152
209
  skippedNewMessages.current = false
153
210
  fetchNewer(newestMessageId)
154
- }, [fetchNewer, newestMessageId, readingHistory])
211
+ }, [caughtUp, fetchNewer, newestMessageId, readingHistory])
155
212
 
156
213
  useEffect(() => {
157
214
  if (reply_root_id) {
@@ -201,6 +258,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
201
258
  conversationId={conversation_id}
202
259
  messages={messages}
203
260
  replyRootId={reply_root_id}
261
+ anchorMessageId={anchor}
204
262
  hasNextPage={hasNextPage}
205
263
  onEndReached={() => fetchNextPage()}
206
264
  onScroll={trackScroll}
@@ -208,10 +266,18 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
208
266
  onContentSizeChange={onContentSizeChange}
209
267
  onScrollEndDrag={onScrollEndDrag}
210
268
  onMomentumScrollEnd={onMomentumScrollEnd}
211
- maintainVisibleContentPosition={readingHistory ? { minIndexForVisible: 0 } : undefined}
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
+ }
212
275
  />
213
276
  )}
214
- <JumpToBottomButton onPress={jumpToLatest} visible={showJumpToBottomButton} />
277
+ <JumpToBottomButton
278
+ onPress={jumpToLatest}
279
+ visible={behindLatest || showJumpToBottomButton}
280
+ />
215
281
  {!noMessages && <TypingIndicator />}
216
282
  {showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}
217
283
  <ConversationBottomBar
@@ -279,6 +345,11 @@ function ConversationBottomBar({
279
345
  )
280
346
  }
281
347
 
348
+ const isBeforeLatestMessage = (anchor?: string | null, latestMessageCreatedAt?: string) =>
349
+ !!anchor &&
350
+ !!latestMessageCreatedAt &&
351
+ decodeUlidTime(anchor) < Date.parse(latestMessageCreatedAt)
352
+
282
353
  interface ConversationScreenTitleProps extends HeaderTitleProps {
283
354
  conversation_id: number
284
355
  badge?: ConversationBadgeResource
@@ -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++) {
@@ -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
- conversation_id: number
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
- conversation_id,
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
+ }